Skip to content

Commit

Permalink
Add withdraw excess with test cases (#35)
Browse files Browse the repository at this point in the history
* Add withdraw excess with test cases

* Fix idl

* Arrange test case according to best practice

* Apply best practices to test and order idl.json

* Fix conflict in idl.json

* Update code according to review

* Add idl changes

* Review update

* Formatting

* Fix conflict

* Revert idl.json

* Add withdraw excess into idl.json

* Fix test case

* Change the owner to payer
  • Loading branch information
DogukanGun authored Nov 14, 2024
1 parent 8dbae66 commit 8947647
Show file tree
Hide file tree
Showing 5 changed files with 412 additions and 1 deletion.
1 change: 1 addition & 0 deletions clients/js/src/generated/instructions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export * from './updateTokenGroupUpdateAuthority';
export * from './updateTokenMetadataField';
export * from './updateTokenMetadataUpdateAuthority';
export * from './updateTransferHook';
export * from './withdrawExcessLamports';
export * from './withdrawWithheldTokensFromAccounts';
export * from './withdrawWithheldTokensFromAccountsForConfidentialTransferFee';
export * from './withdrawWithheldTokensFromMint';
Expand Down
223 changes: 223 additions & 0 deletions clients/js/src/generated/instructions/withdrawExcessLamports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/**
* This code was AUTOGENERATED using the codama library.
* Please DO NOT EDIT THIS FILE, instead use visitors
* to add features, then rerun codama to update it.
*
* @see https://github.com/codama-idl/codama
*/

import {
AccountRole,
combineCodec,
getStructDecoder,
getStructEncoder,
getU8Decoder,
getU8Encoder,
transformEncoder,
type Address,
type Codec,
type Decoder,
type Encoder,
type IAccountMeta,
type IAccountSignerMeta,
type IInstruction,
type IInstructionWithAccounts,
type IInstructionWithData,
type ReadonlyAccount,
type ReadonlySignerAccount,
type TransactionSigner,
type WritableAccount,
} from '@solana/web3.js';
import { TOKEN_2022_PROGRAM_ADDRESS } from '../programs';
import { getAccountMetaFactory, type ResolvedAccount } from '../shared';

export const WITHDRAW_EXCESS_LAMPORTS_DISCRIMINATOR = 38;

export function getWithdrawExcessLamportsDiscriminatorBytes() {
return getU8Encoder().encode(WITHDRAW_EXCESS_LAMPORTS_DISCRIMINATOR);
}

export type WithdrawExcessLamportsInstruction<
TProgram extends string = typeof TOKEN_2022_PROGRAM_ADDRESS,
TAccountSourceAccount extends string | IAccountMeta<string> = string,
TAccountDestinationAccount extends string | IAccountMeta<string> = string,
TAccountAuthority extends string | IAccountMeta<string> = string,
TRemainingAccounts extends readonly IAccountMeta<string>[] = [],
> = IInstruction<TProgram> &
IInstructionWithData<Uint8Array> &
IInstructionWithAccounts<
[
TAccountSourceAccount extends string
? WritableAccount<TAccountSourceAccount>
: TAccountSourceAccount,
TAccountDestinationAccount extends string
? WritableAccount<TAccountDestinationAccount>
: TAccountDestinationAccount,
TAccountAuthority extends string
? ReadonlyAccount<TAccountAuthority>
: TAccountAuthority,
...TRemainingAccounts,
]
>;

export type WithdrawExcessLamportsInstructionData = { discriminator: number };

export type WithdrawExcessLamportsInstructionDataArgs = {};

export function getWithdrawExcessLamportsInstructionDataEncoder(): Encoder<WithdrawExcessLamportsInstructionDataArgs> {
return transformEncoder(
getStructEncoder([['discriminator', getU8Encoder()]]),
(value) => ({
...value,
discriminator: WITHDRAW_EXCESS_LAMPORTS_DISCRIMINATOR,
})
);
}

export function getWithdrawExcessLamportsInstructionDataDecoder(): Decoder<WithdrawExcessLamportsInstructionData> {
return getStructDecoder([['discriminator', getU8Decoder()]]);
}

export function getWithdrawExcessLamportsInstructionDataCodec(): Codec<
WithdrawExcessLamportsInstructionDataArgs,
WithdrawExcessLamportsInstructionData
> {
return combineCodec(
getWithdrawExcessLamportsInstructionDataEncoder(),
getWithdrawExcessLamportsInstructionDataDecoder()
);
}

export type WithdrawExcessLamportsInput<
TAccountSourceAccount extends string = string,
TAccountDestinationAccount extends string = string,
TAccountAuthority extends string = string,
> = {
/** Account holding excess lamports. */
sourceAccount: Address<TAccountSourceAccount>;
/** Destination account for withdrawn lamports. */
destinationAccount: Address<TAccountDestinationAccount>;
/** The source account's owner/delegate or its multisignature account. */
authority: Address<TAccountAuthority> | TransactionSigner<TAccountAuthority>;
multiSigners?: Array<TransactionSigner>;
};

export function getWithdrawExcessLamportsInstruction<
TAccountSourceAccount extends string,
TAccountDestinationAccount extends string,
TAccountAuthority extends string,
TProgramAddress extends Address = typeof TOKEN_2022_PROGRAM_ADDRESS,
>(
input: WithdrawExcessLamportsInput<
TAccountSourceAccount,
TAccountDestinationAccount,
TAccountAuthority
>,
config?: { programAddress?: TProgramAddress }
): WithdrawExcessLamportsInstruction<
TProgramAddress,
TAccountSourceAccount,
TAccountDestinationAccount,
(typeof input)['authority'] extends TransactionSigner<TAccountAuthority>
? ReadonlySignerAccount<TAccountAuthority> &
IAccountSignerMeta<TAccountAuthority>
: TAccountAuthority
> {
// Program address.
const programAddress = config?.programAddress ?? TOKEN_2022_PROGRAM_ADDRESS;

// Original accounts.
const originalAccounts = {
sourceAccount: { value: input.sourceAccount ?? null, isWritable: true },
destinationAccount: {
value: input.destinationAccount ?? null,
isWritable: true,
},
authority: { value: input.authority ?? null, isWritable: false },
};
const accounts = originalAccounts as Record<
keyof typeof originalAccounts,
ResolvedAccount
>;

// Original args.
const args = { ...input };

// Remaining accounts.
const remainingAccounts: IAccountMeta[] = (args.multiSigners ?? []).map(
(signer) => ({
address: signer.address,
role: AccountRole.READONLY_SIGNER,
signer,
})
);

const getAccountMeta = getAccountMetaFactory(programAddress, 'programId');
const instruction = {
accounts: [
getAccountMeta(accounts.sourceAccount),
getAccountMeta(accounts.destinationAccount),
getAccountMeta(accounts.authority),
...remainingAccounts,
],
programAddress,
data: getWithdrawExcessLamportsInstructionDataEncoder().encode({}),
} as WithdrawExcessLamportsInstruction<
TProgramAddress,
TAccountSourceAccount,
TAccountDestinationAccount,
(typeof input)['authority'] extends TransactionSigner<TAccountAuthority>
? ReadonlySignerAccount<TAccountAuthority> &
IAccountSignerMeta<TAccountAuthority>
: TAccountAuthority
>;

return instruction;
}

export type ParsedWithdrawExcessLamportsInstruction<
TProgram extends string = typeof TOKEN_2022_PROGRAM_ADDRESS,
TAccountMetas extends readonly IAccountMeta[] = readonly IAccountMeta[],
> = {
programAddress: Address<TProgram>;
accounts: {
/** Account holding excess lamports. */
sourceAccount: TAccountMetas[0];
/** Destination account for withdrawn lamports. */
destinationAccount: TAccountMetas[1];
/** The source account's owner/delegate or its multisignature account. */
authority: TAccountMetas[2];
};
data: WithdrawExcessLamportsInstructionData;
};

export function parseWithdrawExcessLamportsInstruction<
TProgram extends string,
TAccountMetas extends readonly IAccountMeta[],
>(
instruction: IInstruction<TProgram> &
IInstructionWithAccounts<TAccountMetas> &
IInstructionWithData<Uint8Array>
): ParsedWithdrawExcessLamportsInstruction<TProgram, TAccountMetas> {
if (instruction.accounts.length < 3) {
// TODO: Coded error.
throw new Error('Not enough accounts');
}
let accountIndex = 0;
const getNextAccount = () => {
const accountMeta = instruction.accounts![accountIndex]!;
accountIndex += 1;
return accountMeta;
};
return {
programAddress: instruction.programAddress,
accounts: {
sourceAccount: getNextAccount(),
destinationAccount: getNextAccount(),
authority: getNextAccount(),
},
data: getWithdrawExcessLamportsInstructionDataDecoder().decode(
instruction.data
),
};
}
8 changes: 8 additions & 0 deletions clients/js/src/generated/programs/token2022.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import {
type ParsedUpdateTokenMetadataFieldInstruction,
type ParsedUpdateTokenMetadataUpdateAuthorityInstruction,
type ParsedUpdateTransferHookInstruction,
type ParsedWithdrawExcessLamportsInstruction,
type ParsedWithdrawWithheldTokensFromAccountsForConfidentialTransferFeeInstruction,
type ParsedWithdrawWithheldTokensFromAccountsInstruction,
type ParsedWithdrawWithheldTokensFromMintForConfidentialTransferFeeInstruction,
Expand Down Expand Up @@ -186,6 +187,7 @@ export enum Token2022Instruction {
HarvestWithheldTokensToMintForConfidentialTransferFee,
EnableHarvestToMint,
DisableHarvestToMint,
WithdrawExcessLamports,
InitializeMetadataPointer,
UpdateMetadataPointer,
InitializeGroupPointer,
Expand Down Expand Up @@ -501,6 +503,9 @@ export function identifyToken2022Instruction(
) {
return Token2022Instruction.DisableHarvestToMint;
}
if (containsBytes(data, getU8Encoder().encode(38), 0)) {
return Token2022Instruction.WithdrawExcessLamports;
}
if (
containsBytes(data, getU8Encoder().encode(39), 0) &&
containsBytes(data, getU8Encoder().encode(0), 1)
Expand Down Expand Up @@ -810,6 +815,9 @@ export type ParsedToken2022Instruction<
| ({
instructionType: Token2022Instruction.DisableHarvestToMint;
} & ParsedDisableHarvestToMintInstruction<TProgram>)
| ({
instructionType: Token2022Instruction.WithdrawExcessLamports;
} & ParsedWithdrawExcessLamportsInstruction<TProgram>)
| ({
instructionType: Token2022Instruction.InitializeMetadataPointer;
} & ParsedInitializeMetadataPointerInstruction<TProgram>)
Expand Down
102 changes: 102 additions & 0 deletions clients/js/test/withdrawExcess.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import test from 'ava';
import {
AccountState,
TOKEN_2022_PROGRAM_ADDRESS,
fetchToken,
findAssociatedTokenPda,
getCreateAssociatedTokenInstructionAsync,
getMintToInstruction,
getWithdrawExcessLamportsInstruction,
} from '../src';
import {
createDefaultSolanaClient,
createMint,
createToken,
generateKeyPairSignerWithSol,
sendAndConfirmInstructions,
} from './_setup';
import { getTransferSolInstruction } from '@solana-program/system';
import { generateKeyPairSigner } from '@solana/web3.js';

test('it withdraws excess lamports from an associated token account', async (t) => {
// Given: A client, a payer, mint authority, token owner, and destination account
const client = createDefaultSolanaClient();
const [payer, mintAuthority, owner, destination] = await Promise.all([
generateKeyPairSignerWithSol(client, 200_000_000n),
generateKeyPairSigner(),
generateKeyPairSigner(),
generateKeyPairSigner(),
]);

// And a mint and token are created
const mint = await createMint({
client,
payer,
authority: mintAuthority,
decimals: 9,
});
const token = await createToken({ client, payer, mint, owner });

// And tokens are minted to the token account
const mintToInstruction = getMintToInstruction({
mint,
token,
mintAuthority,
amount: 100_000n,
});
await sendAndConfirmInstructions(client, payer, [mintToInstruction]);

// And an associated token account (ATA) is created for the owner
const createAtaInstruction = await getCreateAssociatedTokenInstructionAsync({
payer,
mint,
owner: owner.address,
});
await sendAndConfirmInstructions(client, payer, [createAtaInstruction]);

const [ata] = await findAssociatedTokenPda({
mint,
owner: owner.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS,
});

// Ensure the token account was initialized correctly
const initialTokenAccount = await fetchToken(client.rpc, ata);
t.is(initialTokenAccount.data.state, AccountState.Initialized);

// When: SOL is mistakenly transferred to the ATA
const transferSolInstruction = await getTransferSolInstruction({
source: payer,
destination: ata,
amount: 1_000_000n,
});
await sendAndConfirmInstructions(client, payer, [transferSolInstruction]);

// Capture initial balances for comparison after withdrawal
const lamportsBefore = await client.rpc
.getBalance(destination.address)
.send();
const ataLamportsBefore = await client.rpc.getBalance(ata).send();

// And we initiate withdrawal of excess lamports from the ATA to the destination
const withdrawInstruction = await getWithdrawExcessLamportsInstruction({
sourceAccount: ata,
destinationAccount: destination.address,
authority: owner,
});
await sendAndConfirmInstructions(client, payer, [withdrawInstruction]);

// Then: Verify that lamports were successfully withdrawn to the destination
const lamportsAfter = await client.rpc.getBalance(destination.address).send();
const ataLamportsAfter = await client.rpc.getBalance(ata).send();

// Assertions to confirm successful transfer of lamports
t.true(
Number(lamportsAfter.value) > Number(lamportsBefore.value),
'Lamports were successfully withdrawn to the destination account.'
);
t.true(
Number(ataLamportsBefore.value) > Number(ataLamportsAfter.value),
'Lamports were successfully withdrawn from the ATA.'
);
});
Loading

0 comments on commit 8947647

Please sign in to comment.