Skip to content

Commit 9f494be

Browse files
committed
feat: unstake function
1 parent 3f2189b commit 9f494be

File tree

7 files changed

+230
-1
lines changed

7 files changed

+230
-1
lines changed

projects/beets-lst/functions/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { getStakedBalance } from './getStakedBalance';
22
export { stake } from './stake';
3+
export { unStake } from './unStake';

projects/beets-lst/functions/stake.ts

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ interface Props {
88
account: Address;
99
amount: string;
1010
}
11+
12+
/**
13+
* Stake Sonic tokens (S) in Beets.fi liquid staking module
14+
*/
1115
export async function stake({ chainName, account, amount }: Props, { sendTransactions, getProvider, notify }: FunctionOptions): Promise<FunctionReturn> {
1216
if (!account) return toResult('Wallet not connected', true);
1317

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { Address, encodeFunctionData, parseUnits } from 'viem';
2+
import { FunctionReturn, FunctionOptions, TransactionParams, toResult, getChainFromName } from '@heyanon/sdk';
3+
import { supportedChains, STS_ADDRESS } from '../constants';
4+
import { stsAbi } from '../abis';
5+
import { fetchValidators, findHighestDelegatedValidator } from '../helpers/client';
6+
7+
interface Props {
8+
chainName: string;
9+
account: Address;
10+
amount: string;
11+
}
12+
13+
/**
14+
* Unstake staked Sonic tokens (stS)
15+
*
16+
* This action will initiate the undelegation of staked Sonic tokens (stS)
17+
* from the Beets.fi liquid staking module, which will take 14 days to complete.
18+
*
19+
* After 14 days, the user will be able to withdraw their Sonic tokens (S)
20+
* using the `withdraw` function.
21+
*/
22+
export async function unStake({ chainName, account, amount }: Props, { sendTransactions, notify, getProvider }: FunctionOptions): Promise<FunctionReturn> {
23+
if (!account) return toResult('Wallet not connected', true);
24+
25+
const chainId = getChainFromName(chainName);
26+
if (!chainId) return toResult(`Unsupported chain name: ${chainName}`, true);
27+
if (!supportedChains.includes(chainId)) return toResult(`Beets protocol is not supported on ${chainName}`, true);
28+
29+
const amountInWei = parseUnits(amount, 18);
30+
if (amountInWei === 0n) return toResult('Amount must be greater than 0', true);
31+
32+
// Get the public client to read contract data
33+
const publicClient = getProvider(chainId);
34+
35+
// Convert asset amount to shares amount
36+
const sharesAmount = await publicClient.readContract({
37+
address: STS_ADDRESS,
38+
abi: stsAbi,
39+
functionName: 'convertToShares',
40+
args: [amountInWei],
41+
});
42+
43+
await notify('Fetching staking data...');
44+
45+
try {
46+
const validators = await fetchValidators();
47+
if (!validators.length) {
48+
return toResult('No validators found', true);
49+
}
50+
51+
const targetValidator = findHighestDelegatedValidator(validators);
52+
53+
if (BigInt(targetValidator.assetsDelegated) < amountInWei) {
54+
return toResult(`Validator does not have enough staked assets. Maximum available: ${targetValidator.assetsDelegated}`, true);
55+
}
56+
57+
await notify('Preparing to undelegate Sonic tokens from Beets.fi liquid staking module...');
58+
59+
const transactions: TransactionParams[] = [];
60+
const tx: TransactionParams = {
61+
target: STS_ADDRESS,
62+
data: encodeFunctionData({
63+
abi: stsAbi,
64+
functionName: 'undelegateMany',
65+
args: [
66+
[BigInt(targetValidator.validatorId)], // validatorIds array
67+
[sharesAmount], // amountShares array - now using converted shares amount
68+
],
69+
}),
70+
};
71+
transactions.push(tx);
72+
73+
await notify('Waiting for transaction confirmation...');
74+
75+
const result = await sendTransactions({ chainId, account, transactions });
76+
return toResult(
77+
result.isMultisig
78+
? result.data[result.data.length - 1].message
79+
: `Successfully initiated undelegation of ${amount} stS from Beets.fi liquid staking module. You can withdraw your Sonic tokens after 14 days.`,
80+
);
81+
} catch (error) {
82+
return toResult(`Failed to undelegate: ${error instanceof Error ? error.message : 'Unknown error'}`, true);
83+
}
84+
}

projects/beets-lst/helpers/client.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import axios from 'axios';
2+
3+
interface ValidatorData {
4+
validatorId: string;
5+
assetsDelegated: string;
6+
}
7+
8+
const BEETS_API_URL = 'https://backend-v3.beets-ftm-node.com';
9+
10+
/**
11+
* Execute a GraphQL query and return the response as a JSON object.
12+
*/
13+
export async function executeGraphQLQuery<T>(query: string): Promise<T> {
14+
try {
15+
const response = await axios.post(BEETS_API_URL, { query });
16+
17+
if (!response.data?.data) {
18+
throw new Error('Invalid response format from API');
19+
}
20+
21+
return response.data.data as T;
22+
} catch (error) {
23+
if (axios.isAxiosError(error)) {
24+
throw new Error(`GraphQL query failed: ${error.message}`);
25+
}
26+
throw new Error(`Unexpected error during GraphQL query: ${error instanceof Error ? error.message : 'Unknown error'}`);
27+
}
28+
}
29+
30+
/**
31+
* Fetch the list of validators from the BEETS API.
32+
*/
33+
export async function fetchValidators(): Promise<ValidatorData[]> {
34+
const query = `
35+
query {
36+
stsGetGqlStakedSonicData {
37+
delegatedValidators {
38+
validatorId
39+
assetsDelegated
40+
}
41+
}
42+
}
43+
`;
44+
45+
const response = await executeGraphQLQuery<{
46+
stsGetGqlStakedSonicData: {
47+
delegatedValidators: ValidatorData[];
48+
};
49+
}>(query);
50+
return response.stsGetGqlStakedSonicData.delegatedValidators;
51+
}
52+
53+
/**
54+
* Find the validator with the highest amount of assets delegated.
55+
*/
56+
export function findHighestDelegatedValidator(validators: ValidatorData[]): ValidatorData {
57+
return validators.reduce((max, current) => (BigInt(current.assetsDelegated) > BigInt(max.assetsDelegated) ? current : max));
58+
}

projects/beets-lst/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"yarn": "yarn install"
66
},
77
"dependencies": {
8-
"@heyanon/sdk": "^1.0.4"
8+
"@heyanon/sdk": "^1.0.4",
9+
"axios": "^1.7.9"
910
},
1011
"license": "MIT",
1112
"engines": {

projects/beets-lst/tools.ts

+24
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,28 @@ export const tools: AiTool[] = [
4343
},
4444
],
4545
},
46+
// TODO: check if "unstake all of my stS" works
47+
{
48+
name: 'unStake',
49+
description: 'Initiate undelegation of staked Sonic tokens (stS). Tokens can be withdrawn after 14 days.',
50+
required: ['chainName', 'account', 'amount'],
51+
props: [
52+
{
53+
name: 'chainName',
54+
type: 'string',
55+
enum: supportedChains.map(getChainName),
56+
description: 'Name of chain where to unstake tokens',
57+
},
58+
{
59+
name: 'account',
60+
type: 'string',
61+
description: 'Account address that will unstake tokens',
62+
},
63+
{
64+
name: 'amount',
65+
type: 'string',
66+
description: 'Amount of stS tokens to undelegate in decimal format',
67+
},
68+
],
69+
},
4670
];

projects/beets-lst/yarn.lock

+57
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,20 @@ assertion-error@^2.0.1:
474474
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7"
475475
integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==
476476

477+
asynckit@^0.4.0:
478+
version "0.4.0"
479+
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
480+
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
481+
482+
axios@^1.7.9:
483+
version "1.7.9"
484+
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a"
485+
integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==
486+
dependencies:
487+
follow-redirects "^1.15.6"
488+
form-data "^4.0.0"
489+
proxy-from-env "^1.1.0"
490+
477491
big.js@^6.2.1:
478492
version "6.2.2"
479493
resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.2.2.tgz#be3bb9ac834558b53b099deef2a1d06ac6368e1a"
@@ -520,6 +534,13 @@ check-error@^2.1.1:
520534
resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc"
521535
integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==
522536

537+
combined-stream@^1.0.8:
538+
version "1.0.8"
539+
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
540+
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
541+
dependencies:
542+
delayed-stream "~1.0.0"
543+
523544
confbox@^0.1.8:
524545
version "0.1.8"
525546
resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06"
@@ -558,6 +579,11 @@ deep-eql@^5.0.1:
558579
resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341"
559580
integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==
560581

582+
delayed-stream@~1.0.0:
583+
version "1.0.0"
584+
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
585+
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
586+
561587
diff-sequences@^29.6.3:
562588
version "29.6.3"
563589
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921"
@@ -629,6 +655,20 @@ expect-type@^1.1.0:
629655
resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.1.0.tgz#a146e414250d13dfc49eafcfd1344a4060fa4c75"
630656
integrity sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==
631657

658+
follow-redirects@^1.15.6:
659+
version "1.15.9"
660+
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
661+
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
662+
663+
form-data@^4.0.0:
664+
version "4.0.1"
665+
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48"
666+
integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==
667+
dependencies:
668+
asynckit "^0.4.0"
669+
combined-stream "^1.0.8"
670+
mime-types "^2.1.12"
671+
632672
fsevents@~2.3.2, fsevents@~2.3.3:
633673
version "2.3.3"
634674
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
@@ -701,6 +741,18 @@ merge-stream@^2.0.0:
701741
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
702742
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
703743

744+
745+
version "1.52.0"
746+
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
747+
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
748+
749+
mime-types@^2.1.12:
750+
version "2.1.35"
751+
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
752+
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
753+
dependencies:
754+
mime-db "1.52.0"
755+
704756
mimic-fn@^4.0.0:
705757
version "4.0.0"
706758
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"
@@ -827,6 +879,11 @@ pretty-format@^29.7.0:
827879
ansi-styles "^5.0.0"
828880
react-is "^18.0.0"
829881

882+
proxy-from-env@^1.1.0:
883+
version "1.1.0"
884+
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
885+
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
886+
830887
react-is@^18.0.0:
831888
version "18.3.1"
832889
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"

0 commit comments

Comments
 (0)