Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add withdraw excess with test cases #35

Merged
merged 19 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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