Skip to content

Commit ed7a249

Browse files
authored
CloseStuckEscrow generated js methods (#149)
CloseStuckEscrow js helper
1 parent 62ea190 commit ed7a249

File tree

9 files changed

+476
-5
lines changed

9 files changed

+476
-5
lines changed

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ advantage of some of the latest features of a specific token program, this might
1818

1919
## How It Works
2020

21-
It supports three primary operations:
21+
It supports four primary operations:
2222

2323
1. **`CreateMint`:** This operation initializes a new wrapped token mint and its associated backpointer account. Note,
2424
the caller must pre-fund this account with lamports. This is to avoid requiring writer+signer privileges on this
@@ -44,6 +44,17 @@ It supports three primary operations:
4444
* An equivalent amount of unwrapped tokens is transferred from the escrow account to the user's unwrapped token
4545
account.
4646

47+
4. **`CloseStuckEscrow`:** This operation handles an edge case with re-creating a mint with the MintCloseAuthority
48+
extension.
49+
50+
* The escrow ATA can get "stuck" when an unwrapped mint with a close authority is closed and then a new mint is
51+
created at the same address but with different extensions, leaving the escrow ATA (Associated Token Account) in an
52+
incompatible state.
53+
* The instruction closes the old escrow ATA and returns the lamports to a specified destination account.
54+
* This operation will only succeed if the current escrow has zero balance and has different extensions than the
55+
mint.
56+
* After closing the stuck escrow, the client is responsible for recreating the ATA with the correct extensions.
57+
4758
The 1:1 relationship between wrapped and unwrapped tokens is maintained through the escrow mechanism, ensuring that
4859
wrapped tokens are always fully backed by their unwrapped counterparts.
4960

clients/js/CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @solana-program/token-wrap
22

3+
## 2.1.0
4+
5+
### Minor Changes
6+
7+
- Generated methods for CloseStuckEscrow
8+
39
## 2.0.0
410

511
### Major Changes
@@ -11,7 +17,6 @@
1117
### Major Changes
1218

1319
- First stable release
14-
1520
- expose createMintTx, singleSignerWrapTx, singleSignerUnwrapTx
1621
- provide multisig-helper builders and utilities (combinedMultisigTx, escrow creation, token-account helpers)
1722
- ship generated TypeScript types, codecs, PDA finders and error maps

clients/js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@solana-program/token-wrap",
3-
"version": "2.0.0",
3+
"version": "2.1.0",
44
"description": "Javascript helpers for interacting with the Solana Token Wrap program",
55
"author": "Anza Maintainers <[email protected]>",
66
"license": "Apache-2.0",

clients/js/src/generated/errors/tokenWrap.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ export const TOKEN_WRAP_ERROR__INVALID_BACKPOINTER_OWNER = 0x6; // 6
3030

3131
export const TOKEN_WRAP_ERROR__ESCROW_MISMATCH = 0x7; // 7
3232

33+
export const TOKEN_WRAP_ERROR__ESCROW_IN_GOOD_STATE = 0x8; // 8
34+
3335
export type TokenWrapError =
3436
| typeof TOKEN_WRAP_ERROR__BACKPOINTER_MISMATCH
37+
| typeof TOKEN_WRAP_ERROR__ESCROW_IN_GOOD_STATE
3538
| typeof TOKEN_WRAP_ERROR__ESCROW_MISMATCH
3639
| typeof TOKEN_WRAP_ERROR__ESCROW_OWNER_MISMATCH
3740
| typeof TOKEN_WRAP_ERROR__INVALID_BACKPOINTER_OWNER
@@ -44,6 +47,7 @@ let tokenWrapErrorMessages: Record<TokenWrapError, string> | undefined;
4447
if (process.env.NODE_ENV !== 'production') {
4548
tokenWrapErrorMessages = {
4649
[TOKEN_WRAP_ERROR__BACKPOINTER_MISMATCH]: `Wrapped backpointer account address does not match expected PDA`,
50+
[TOKEN_WRAP_ERROR__ESCROW_IN_GOOD_STATE]: `The escrow account is in a good state and cannot be recreated`,
4751
[TOKEN_WRAP_ERROR__ESCROW_MISMATCH]: `Escrow account address does not match expected ATA`,
4852
[TOKEN_WRAP_ERROR__ESCROW_OWNER_MISMATCH]: `Unwrapped escrow token owner is not set to expected PDA`,
4953
[TOKEN_WRAP_ERROR__INVALID_BACKPOINTER_OWNER]: `Wrapped backpointer account owner is not the expected token wrap program`,
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/**
2+
* This code was AUTOGENERATED using the codama library.
3+
* Please DO NOT EDIT THIS FILE, instead use visitors
4+
* to add features, then rerun codama to update it.
5+
*
6+
* @see https://github.com/codama-idl/codama
7+
*/
8+
9+
import {
10+
combineCodec,
11+
getStructDecoder,
12+
getStructEncoder,
13+
getU8Decoder,
14+
getU8Encoder,
15+
transformEncoder,
16+
type Address,
17+
type Codec,
18+
type Decoder,
19+
type Encoder,
20+
type IAccountMeta,
21+
type IInstruction,
22+
type IInstructionWithAccounts,
23+
type IInstructionWithData,
24+
type ReadonlyAccount,
25+
type WritableAccount,
26+
} from '@solana/kit';
27+
import { TOKEN_WRAP_PROGRAM_ADDRESS } from '../programs';
28+
import { getAccountMetaFactory, type ResolvedAccount } from '../shared';
29+
30+
export const CLOSE_STUCK_ESCROW_DISCRIMINATOR = 3;
31+
32+
export function getCloseStuckEscrowDiscriminatorBytes() {
33+
return getU8Encoder().encode(CLOSE_STUCK_ESCROW_DISCRIMINATOR);
34+
}
35+
36+
export type CloseStuckEscrowInstruction<
37+
TProgram extends string = typeof TOKEN_WRAP_PROGRAM_ADDRESS,
38+
TAccountEscrow extends string | IAccountMeta<string> = string,
39+
TAccountDestination extends string | IAccountMeta<string> = string,
40+
TAccountUnwrappedMint extends string | IAccountMeta<string> = string,
41+
TAccountWrappedMint extends string | IAccountMeta<string> = string,
42+
TAccountWrappedMintAuthority extends string | IAccountMeta<string> = string,
43+
TAccountToken2022Program extends
44+
| string
45+
| IAccountMeta<string> = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb',
46+
TRemainingAccounts extends readonly IAccountMeta<string>[] = [],
47+
> = IInstruction<TProgram> &
48+
IInstructionWithData<Uint8Array> &
49+
IInstructionWithAccounts<
50+
[
51+
TAccountEscrow extends string
52+
? WritableAccount<TAccountEscrow>
53+
: TAccountEscrow,
54+
TAccountDestination extends string
55+
? WritableAccount<TAccountDestination>
56+
: TAccountDestination,
57+
TAccountUnwrappedMint extends string
58+
? ReadonlyAccount<TAccountUnwrappedMint>
59+
: TAccountUnwrappedMint,
60+
TAccountWrappedMint extends string
61+
? ReadonlyAccount<TAccountWrappedMint>
62+
: TAccountWrappedMint,
63+
TAccountWrappedMintAuthority extends string
64+
? ReadonlyAccount<TAccountWrappedMintAuthority>
65+
: TAccountWrappedMintAuthority,
66+
TAccountToken2022Program extends string
67+
? ReadonlyAccount<TAccountToken2022Program>
68+
: TAccountToken2022Program,
69+
...TRemainingAccounts,
70+
]
71+
>;
72+
73+
export type CloseStuckEscrowInstructionData = { discriminator: number };
74+
75+
export type CloseStuckEscrowInstructionDataArgs = {};
76+
77+
export function getCloseStuckEscrowInstructionDataEncoder(): Encoder<CloseStuckEscrowInstructionDataArgs> {
78+
return transformEncoder(
79+
getStructEncoder([['discriminator', getU8Encoder()]]),
80+
(value) => ({ ...value, discriminator: CLOSE_STUCK_ESCROW_DISCRIMINATOR })
81+
);
82+
}
83+
84+
export function getCloseStuckEscrowInstructionDataDecoder(): Decoder<CloseStuckEscrowInstructionData> {
85+
return getStructDecoder([['discriminator', getU8Decoder()]]);
86+
}
87+
88+
export function getCloseStuckEscrowInstructionDataCodec(): Codec<
89+
CloseStuckEscrowInstructionDataArgs,
90+
CloseStuckEscrowInstructionData
91+
> {
92+
return combineCodec(
93+
getCloseStuckEscrowInstructionDataEncoder(),
94+
getCloseStuckEscrowInstructionDataDecoder()
95+
);
96+
}
97+
98+
export type CloseStuckEscrowInput<
99+
TAccountEscrow extends string = string,
100+
TAccountDestination extends string = string,
101+
TAccountUnwrappedMint extends string = string,
102+
TAccountWrappedMint extends string = string,
103+
TAccountWrappedMintAuthority extends string = string,
104+
TAccountToken2022Program extends string = string,
105+
> = {
106+
/** Escrow account to close (ATA) */
107+
escrow: Address<TAccountEscrow>;
108+
/** Destination for lamports from closed account */
109+
destination: Address<TAccountDestination>;
110+
/** Unwrapped mint */
111+
unwrappedMint: Address<TAccountUnwrappedMint>;
112+
/** Wrapped mint */
113+
wrappedMint: Address<TAccountWrappedMint>;
114+
/** Wrapped mint authority (PDA) */
115+
wrappedMintAuthority: Address<TAccountWrappedMintAuthority>;
116+
/** Token-2022 program */
117+
token2022Program?: Address<TAccountToken2022Program>;
118+
};
119+
120+
export function getCloseStuckEscrowInstruction<
121+
TAccountEscrow extends string,
122+
TAccountDestination extends string,
123+
TAccountUnwrappedMint extends string,
124+
TAccountWrappedMint extends string,
125+
TAccountWrappedMintAuthority extends string,
126+
TAccountToken2022Program extends string,
127+
TProgramAddress extends Address = typeof TOKEN_WRAP_PROGRAM_ADDRESS,
128+
>(
129+
input: CloseStuckEscrowInput<
130+
TAccountEscrow,
131+
TAccountDestination,
132+
TAccountUnwrappedMint,
133+
TAccountWrappedMint,
134+
TAccountWrappedMintAuthority,
135+
TAccountToken2022Program
136+
>,
137+
config?: { programAddress?: TProgramAddress }
138+
): CloseStuckEscrowInstruction<
139+
TProgramAddress,
140+
TAccountEscrow,
141+
TAccountDestination,
142+
TAccountUnwrappedMint,
143+
TAccountWrappedMint,
144+
TAccountWrappedMintAuthority,
145+
TAccountToken2022Program
146+
> {
147+
// Program address.
148+
const programAddress = config?.programAddress ?? TOKEN_WRAP_PROGRAM_ADDRESS;
149+
150+
// Original accounts.
151+
const originalAccounts = {
152+
escrow: { value: input.escrow ?? null, isWritable: true },
153+
destination: { value: input.destination ?? null, isWritable: true },
154+
unwrappedMint: { value: input.unwrappedMint ?? null, isWritable: false },
155+
wrappedMint: { value: input.wrappedMint ?? null, isWritable: false },
156+
wrappedMintAuthority: {
157+
value: input.wrappedMintAuthority ?? null,
158+
isWritable: false,
159+
},
160+
token2022Program: {
161+
value: input.token2022Program ?? null,
162+
isWritable: false,
163+
},
164+
};
165+
const accounts = originalAccounts as Record<
166+
keyof typeof originalAccounts,
167+
ResolvedAccount
168+
>;
169+
170+
// Resolve default values.
171+
if (!accounts.token2022Program.value) {
172+
accounts.token2022Program.value =
173+
'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' as Address<'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'>;
174+
}
175+
176+
const getAccountMeta = getAccountMetaFactory(programAddress, 'programId');
177+
const instruction = {
178+
accounts: [
179+
getAccountMeta(accounts.escrow),
180+
getAccountMeta(accounts.destination),
181+
getAccountMeta(accounts.unwrappedMint),
182+
getAccountMeta(accounts.wrappedMint),
183+
getAccountMeta(accounts.wrappedMintAuthority),
184+
getAccountMeta(accounts.token2022Program),
185+
],
186+
programAddress,
187+
data: getCloseStuckEscrowInstructionDataEncoder().encode({}),
188+
} as CloseStuckEscrowInstruction<
189+
TProgramAddress,
190+
TAccountEscrow,
191+
TAccountDestination,
192+
TAccountUnwrappedMint,
193+
TAccountWrappedMint,
194+
TAccountWrappedMintAuthority,
195+
TAccountToken2022Program
196+
>;
197+
198+
return instruction;
199+
}
200+
201+
export type ParsedCloseStuckEscrowInstruction<
202+
TProgram extends string = typeof TOKEN_WRAP_PROGRAM_ADDRESS,
203+
TAccountMetas extends readonly IAccountMeta[] = readonly IAccountMeta[],
204+
> = {
205+
programAddress: Address<TProgram>;
206+
accounts: {
207+
/** Escrow account to close (ATA) */
208+
escrow: TAccountMetas[0];
209+
/** Destination for lamports from closed account */
210+
destination: TAccountMetas[1];
211+
/** Unwrapped mint */
212+
unwrappedMint: TAccountMetas[2];
213+
/** Wrapped mint */
214+
wrappedMint: TAccountMetas[3];
215+
/** Wrapped mint authority (PDA) */
216+
wrappedMintAuthority: TAccountMetas[4];
217+
/** Token-2022 program */
218+
token2022Program?: TAccountMetas[5] | undefined;
219+
};
220+
data: CloseStuckEscrowInstructionData;
221+
};
222+
223+
export function parseCloseStuckEscrowInstruction<
224+
TProgram extends string,
225+
TAccountMetas extends readonly IAccountMeta[],
226+
>(
227+
instruction: IInstruction<TProgram> &
228+
IInstructionWithAccounts<TAccountMetas> &
229+
IInstructionWithData<Uint8Array>
230+
): ParsedCloseStuckEscrowInstruction<TProgram, TAccountMetas> {
231+
if (instruction.accounts.length < 6) {
232+
// TODO: Coded error.
233+
throw new Error('Not enough accounts');
234+
}
235+
let accountIndex = 0;
236+
const getNextAccount = () => {
237+
const accountMeta = instruction.accounts![accountIndex]!;
238+
accountIndex += 1;
239+
return accountMeta;
240+
};
241+
const getNextOptionalAccount = () => {
242+
const accountMeta = getNextAccount();
243+
return accountMeta.address === TOKEN_WRAP_PROGRAM_ADDRESS
244+
? undefined
245+
: accountMeta;
246+
};
247+
return {
248+
programAddress: instruction.programAddress,
249+
accounts: {
250+
escrow: getNextAccount(),
251+
destination: getNextAccount(),
252+
unwrappedMint: getNextAccount(),
253+
wrappedMint: getNextAccount(),
254+
wrappedMintAuthority: getNextAccount(),
255+
token2022Program: getNextOptionalAccount(),
256+
},
257+
data: getCloseStuckEscrowInstructionDataDecoder().decode(instruction.data),
258+
};
259+
}

clients/js/src/generated/instructions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* @see https://github.com/codama-idl/codama
77
*/
88

9+
export * from './closeStuckEscrow';
910
export * from './createMint';
1011
export * from './unwrap';
1112
export * from './wrap';

clients/js/src/generated/programs/tokenWrap.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type ReadonlyUint8Array,
1414
} from '@solana/kit';
1515
import {
16+
type ParsedCloseStuckEscrowInstruction,
1617
type ParsedCreateMintInstruction,
1718
type ParsedUnwrapInstruction,
1819
type ParsedWrapInstruction,
@@ -29,6 +30,7 @@ export enum TokenWrapInstruction {
2930
CreateMint,
3031
Wrap,
3132
Unwrap,
33+
CloseStuckEscrow,
3234
}
3335

3436
export function identifyTokenWrapInstruction(
@@ -44,6 +46,9 @@ export function identifyTokenWrapInstruction(
4446
if (containsBytes(data, getU8Encoder().encode(2), 0)) {
4547
return TokenWrapInstruction.Unwrap;
4648
}
49+
if (containsBytes(data, getU8Encoder().encode(3), 0)) {
50+
return TokenWrapInstruction.CloseStuckEscrow;
51+
}
4752
throw new Error(
4853
'The provided instruction could not be identified as a tokenWrap instruction.'
4954
);
@@ -60,4 +65,7 @@ export type ParsedTokenWrapInstruction<
6065
} & ParsedWrapInstruction<TProgram>)
6166
| ({
6267
instructionType: TokenWrapInstruction.Unwrap;
63-
} & ParsedUnwrapInstruction<TProgram>);
68+
} & ParsedUnwrapInstruction<TProgram>)
69+
| ({
70+
instructionType: TokenWrapInstruction.CloseStuckEscrow;
71+
} & ParsedCloseStuckEscrowInstruction<TProgram>);

0 commit comments

Comments
 (0)