Skip to content

Commit bde64bc

Browse files
fix(sdk-coin-sol): handling sol 2022 token recoveries
TICKET: WIN-5929
1 parent 5f8e582 commit bde64bc

File tree

3 files changed

+197
-4
lines changed

3 files changed

+197
-4
lines changed

modules/sdk-coin-sol/src/sol.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ import {
5858
isValidPublicKey,
5959
validateRawTransaction,
6060
} from './lib/utils';
61+
import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token';
62+
6163
export const DEFAULT_SCAN_FACTOR = 20; // default number of receive addresses to scan for funds
6264

6365
export interface TransactionFee {
@@ -154,6 +156,7 @@ export interface SolRecoveryOptions extends MPCRecoveryOptions {
154156
closeAtaAddress?: string;
155157
// destination address where token should be sent before closing the ATA address
156158
recoveryDestinationAtaAddress?: string;
159+
programId?: string; // programId of the token
157160
}
158161

159162
export interface SolConsolidationRecoveryOptions extends MPCConsolidationRecoveryOptions {
@@ -638,7 +641,7 @@ export class Sol extends BaseCoin {
638641
};
639642
}
640643

641-
protected async getTokenAccountsByOwner(pubKey = ''): Promise<[] | TokenAccount[]> {
644+
protected async getTokenAccountsByOwner(pubKey = '', programId = ''): Promise<[] | TokenAccount[]> {
642645
const response = await this.getDataFromNode({
643646
payload: {
644647
id: '1',
@@ -647,7 +650,10 @@ export class Sol extends BaseCoin {
647650
params: [
648651
pubKey,
649652
{
650-
programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
653+
programId:
654+
programId.toString().toLowerCase() === TOKEN_2022_PROGRAM_ID.toString().toLowerCase()
655+
? TOKEN_2022_PROGRAM_ID.toString()
656+
: TOKEN_PROGRAM_ID.toString(),
651657
},
652658
{
653659
encoding: 'jsonParsed',
@@ -793,7 +799,7 @@ export class Sol extends BaseCoin {
793799

794800
// check for possible token recovery, recover the token provide by user
795801
if (params.tokenContractAddress) {
796-
const tokenAccounts = await this.getTokenAccountsByOwner(bs58EncodedPublicKey);
802+
const tokenAccounts = await this.getTokenAccountsByOwner(bs58EncodedPublicKey, params.programId);
797803
if (tokenAccounts.length !== 0) {
798804
// there exists token accounts on the given address, but need to check certain conditions:
799805
// 1. if there is a recoverable balance
@@ -824,7 +830,10 @@ export class Sol extends BaseCoin {
824830
.feePayer(bs58EncodedPublicKey);
825831

826832
// need to get all token accounts of the recipient address and need to create them if they do not exist
827-
const recipientTokenAccounts = await this.getTokenAccountsByOwner(params.recoveryDestination);
833+
const recipientTokenAccounts = await this.getTokenAccountsByOwner(
834+
params.recoveryDestination,
835+
params.programId
836+
);
828837

829838
for (const tokenAccount of recovereableTokenAccounts) {
830839
let recipientTokenAccountExists = false;
@@ -851,6 +860,10 @@ export class Sol extends BaseCoin {
851860
txBuilder.createAssociatedTokenAccount({
852861
ownerAddress: params.recoveryDestination,
853862
tokenName: tokenName,
863+
programId:
864+
params.programId?.toString().toLowerCase() === TOKEN_2022_PROGRAM_ID.toString().toLowerCase()
865+
? TOKEN_2022_PROGRAM_ID.toString()
866+
: TOKEN_PROGRAM_ID.toString(),
854867
});
855868
// add rent exempt amount to total fee for each token account that has to be created
856869
totalFeeForTokenRecovery = totalFeeForTokenRecovery.plus(rentExemptAmount);

modules/sdk-coin-sol/test/fixtures/sol.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,96 @@ const getTokenAccountsByOwnerResponse2 = {
411411
},
412412
};
413413

414+
const getTokenAccountsByOwnerForSol2022Response = {
415+
status: 200,
416+
body: {
417+
jsonrpc: '2.0',
418+
result: {
419+
context: {
420+
apiVersion: '1.17.5',
421+
slot: 259019329,
422+
},
423+
value: [
424+
{
425+
account: {
426+
data: {
427+
parsed: {
428+
info: {
429+
isNative: false,
430+
mint: '5NR1bQwLWqjbkhbQ1hx72HKJybbuvwkDnUZNoAZ2VhW6',
431+
owner: 'cyggsFnDvbfsPeiFXziebWsWAp6bW5Nc5SePTx8mebL',
432+
state: 'initialized',
433+
tokenAmount: {
434+
amount: '2000000000',
435+
decimals: 9,
436+
uiAmount: 2,
437+
uiAmountString: '2',
438+
},
439+
},
440+
type: 'account',
441+
},
442+
program: 'spl-token',
443+
space: 165,
444+
},
445+
executable: false,
446+
lamports: 2039280,
447+
owner: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb',
448+
rentEpoch: 18446744073709552000,
449+
space: 165,
450+
},
451+
pubkey: '5seRT43oRMkVgwxEssfci6Zgs9PJkt4igjqRiEjL5g3v',
452+
},
453+
],
454+
},
455+
id: '1',
456+
},
457+
};
458+
459+
const getTokenAccountsByOwnerForSol2022Response2 = {
460+
status: 200,
461+
body: {
462+
jsonrpc: '2.0',
463+
result: {
464+
context: {
465+
apiVersion: '1.17.5',
466+
slot: 259019329,
467+
},
468+
value: [
469+
{
470+
account: {
471+
data: {
472+
parsed: {
473+
info: {
474+
isNative: false,
475+
mint: '5NR1bQwLWqjbkhbQ1hx72HKJybbuvwkDnUZNoAZ2VhW6',
476+
owner: 'HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs',
477+
state: 'initialized',
478+
tokenAmount: {
479+
amount: '2000000000',
480+
decimals: 9,
481+
uiAmount: 2,
482+
uiAmountString: '2',
483+
},
484+
},
485+
type: 'account',
486+
},
487+
program: 'spl-token',
488+
space: 165,
489+
},
490+
executable: false,
491+
lamports: 2039280,
492+
owner: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb',
493+
rentEpoch: 18446744073709552000,
494+
space: 165,
495+
},
496+
pubkey: 'E2ZC9Kgc1aq3q7ect4iyF3zVgJWCfomRGsg98hkJxm6k',
497+
},
498+
],
499+
},
500+
id: '1',
501+
},
502+
};
503+
414504
const getTokenAccountsByOwnerResponseNoAccounts = {
415505
status: 200,
416506
body: {
@@ -502,6 +592,8 @@ export const SolResponses = {
502592
getTokenAccountsByOwnerResponse2,
503593
getTokenAccountsByOwnerResponse3,
504594
getTokenAccountsByOwnerResponseNoAccounts,
595+
getTokenAccountsByOwnerForSol2022Response,
596+
getTokenAccountsByOwnerForSol2022Response2,
505597
getMinimumBalanceForRentExemptionResponse,
506598
broadcastTransactionResponse,
507599
broadcastTransactionResponse1,

modules/sdk-coin-sol/test/unit/sol.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import * as testData from '../fixtures/sol';
1414
import * as resources from '../resources/sol';
1515
import { getBuilderFactory } from './getBuilderFactory';
1616
import { solBackupKey } from './fixtures/solBackupKey';
17+
import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token';
1718

1819
describe('SOL:', function () {
1920
let bitgo: TestBitGoAPI;
@@ -1487,6 +1488,7 @@ describe('SOL:', function () {
14871488
const sandBox = sinon.createSandbox();
14881489
const coin = coins.get('tsol');
14891490
const usdtMintAddress = '9cgpBeNZ2HnLda7NWaaU1i3NyTstk2c4zCMUcoAGsi9C';
1491+
const t22mintAddress = '5NR1bQwLWqjbkhbQ1hx72HKJybbuvwkDnUZNoAZ2VhW6';
14901492
let callBack;
14911493

14921494
beforeEach(() => {
@@ -1685,6 +1687,42 @@ describe('SOL:', function () {
16851687
},
16861688
})
16871689
.resolves(testData.SolResponses.getTokenAccountsByOwnerResponse);
1690+
callBack
1691+
.withArgs({
1692+
payload: {
1693+
id: '1',
1694+
jsonrpc: '2.0',
1695+
method: 'getTokenAccountsByOwner',
1696+
params: [
1697+
testData.keys.destinationPubKey2,
1698+
{
1699+
programId: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb',
1700+
},
1701+
{
1702+
encoding: 'jsonParsed',
1703+
},
1704+
],
1705+
},
1706+
})
1707+
.resolves(testData.SolResponses.getTokenAccountsByOwnerForSol2022Response2);
1708+
callBack
1709+
.withArgs({
1710+
payload: {
1711+
id: '1',
1712+
jsonrpc: '2.0',
1713+
method: 'getTokenAccountsByOwner',
1714+
params: [
1715+
testData.wrwUser.walletAddress0,
1716+
{
1717+
programId: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb',
1718+
},
1719+
{
1720+
encoding: 'jsonParsed',
1721+
},
1722+
],
1723+
},
1724+
})
1725+
.resolves(testData.SolResponses.getTokenAccountsByOwnerForSol2022Response);
16881726
callBack
16891727
.withArgs({
16901728
payload: {
@@ -2019,6 +2057,56 @@ describe('SOL:', function () {
20192057
sandBox.assert.callCount(solCoin.getDataFromNode, 7);
20202058
});
20212059

2060+
it('should recover sol 2022 tokens to recovery destination with existing token accounts', async function () {
2061+
const tokenTxn = await basecoin.recover({
2062+
userKey: testData.wrwUser.userKey,
2063+
backupKey: testData.wrwUser.backupKey,
2064+
bitgoKey: testData.wrwUser.bitgoKey,
2065+
recoveryDestination: testData.keys.destinationPubKey2,
2066+
tokenContractAddress: t22mintAddress,
2067+
walletPassphrase: testData.wrwUser.walletPassphrase,
2068+
durableNonce: {
2069+
publicKey: testData.keys.durableNoncePubKey,
2070+
secretKey: testData.keys.durableNoncePrivKey,
2071+
},
2072+
programId: TOKEN_2022_PROGRAM_ID.toString(),
2073+
});
2074+
2075+
tokenTxn.should.not.be.empty();
2076+
tokenTxn.should.hasOwnProperty('serializedTx');
2077+
tokenTxn.should.hasOwnProperty('scanIndex');
2078+
should.equal((tokenTxn as MPCTx).scanIndex, 0);
2079+
2080+
const tokenTxnDeserialize = new Transaction(coin);
2081+
tokenTxnDeserialize.fromRawTransaction((tokenTxn as MPCTx).serializedTx);
2082+
const tokenTxnJson = tokenTxnDeserialize.toJson();
2083+
console.log(tokenTxnJson);
2084+
should.equal(tokenTxnJson.nonce, testData.SolInputData.durableNonceBlockhash);
2085+
should.equal(tokenTxnJson.feePayer, testData.wrwUser.walletAddress0);
2086+
should.equal(tokenTxnJson.numSignatures, testData.SolInputData.durableNonceSignatures);
2087+
2088+
const instructionsData = tokenTxnJson.instructionsData as TokenTransfer[];
2089+
should.equal(instructionsData.length, 2);
2090+
should.equal(instructionsData[0].type, 'NonceAdvance');
2091+
2092+
const source2022TokenAccount = await getAssociatedTokenAccountAddress(
2093+
t22mintAddress,
2094+
testData.wrwUser.walletAddress0
2095+
);
2096+
const destination2022TokenAccount = await getAssociatedTokenAccountAddress(
2097+
t22mintAddress,
2098+
testData.keys.destinationPubKey2
2099+
);
2100+
should.equal(instructionsData[1].type, 'TokenTransfer');
2101+
should.equal(instructionsData[1].params.fromAddress, testData.wrwUser.walletAddress0);
2102+
should.equal(instructionsData[1].params.toAddress, destination2022TokenAccount);
2103+
should.equal(instructionsData[1].params.amount, '2000000000');
2104+
should.equal(instructionsData[1].params.tokenName, 'tsol:t22mint');
2105+
should.equal(instructionsData[1].params.sourceAddress, source2022TokenAccount);
2106+
const solCoin = basecoin as any;
2107+
sandBox.assert.callCount(solCoin.getDataFromNode, 7);
2108+
});
2109+
20222110
it('should recover sol tokens to recovery destination with existing token accounts for unsigned sweep recoveries', async function () {
20232111
const feeResponse = testData.SolResponses.getFeesForMessageResponse;
20242112
feeResponse.body.result.value = 10000;

0 commit comments

Comments
 (0)