@solana-program/token-wrap 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +20 -19
- package/src/create-mint.ts +125 -0
- package/src/examples/multisig.ts +295 -0
- package/src/examples/single-signer.ts +175 -0
- package/src/examples/sync-spl-to-token2022.ts +96 -0
- package/src/examples/sync-token2022-to-spl.ts +101 -0
- package/src/generated/accounts/backpointer.ts +125 -0
- package/{dist/types/generated/accounts/index.d.ts → src/generated/accounts/index.ts} +1 -0
- package/{dist/types/generated/errors/index.d.ts → src/generated/errors/index.ts} +1 -0
- package/src/generated/errors/tokenWrap.ts +89 -0
- package/{dist/types/generated/index.d.ts → src/generated/index.ts} +1 -0
- package/src/generated/instructions/closeStuckEscrow.ts +235 -0
- package/src/generated/instructions/createMint.ts +250 -0
- package/{dist/types/generated/instructions/index.d.ts → src/generated/instructions/index.ts} +1 -0
- package/src/generated/instructions/syncMetadataToSplToken.ts +305 -0
- package/src/generated/instructions/syncMetadataToToken2022.ts +253 -0
- package/src/generated/instructions/unwrap.ts +326 -0
- package/src/generated/instructions/wrap.ts +326 -0
- package/src/generated/pdas/backpointer.ts +32 -0
- package/{dist/types/generated/pdas/index.d.ts → src/generated/pdas/index.ts} +1 -0
- package/src/generated/pdas/wrappedMint.ts +37 -0
- package/src/generated/pdas/wrappedMintAuthority.ts +32 -0
- package/{dist/types/generated/programs/index.d.ts → src/generated/programs/index.ts} +1 -0
- package/src/generated/programs/tokenWrap.ts +228 -0
- package/src/global.d.ts +8 -0
- package/src/index.ts +23 -0
- package/src/unwrap.ts +208 -0
- package/src/utilities.ts +234 -0
- package/src/wrap.ts +211 -0
- package/dist/src/index.js +0 -1292
- package/dist/src/index.js.map +0 -1
- package/dist/src/index.mjs +0 -1206
- package/dist/src/index.mjs.map +0 -1
- package/dist/types/create-mint.d.ts +0 -16
- package/dist/types/examples/multisig.d.ts +0 -1
- package/dist/types/examples/single-signer.d.ts +0 -1
- package/dist/types/examples/sync-spl-to-token2022.d.ts +0 -1
- package/dist/types/examples/sync-token2022-to-spl.d.ts +0 -1
- package/dist/types/generated/accounts/backpointer.d.ts +0 -29
- package/dist/types/generated/errors/tokenWrap.d.ts +0 -35
- package/dist/types/generated/instructions/closeStuckEscrow.d.ts +0 -63
- package/dist/types/generated/instructions/createMint.d.ts +0 -76
- package/dist/types/generated/instructions/syncMetadataToSplToken.d.ts +0 -92
- package/dist/types/generated/instructions/syncMetadataToToken2022.d.ts +0 -75
- package/dist/types/generated/instructions/unwrap.d.ts +0 -103
- package/dist/types/generated/instructions/wrap.d.ts +0 -103
- package/dist/types/generated/pdas/backpointer.d.ts +0 -14
- package/dist/types/generated/pdas/wrappedMint.d.ts +0 -15
- package/dist/types/generated/pdas/wrappedMintAuthority.d.ts +0 -14
- package/dist/types/generated/programs/tokenWrap.d.ts +0 -37
- package/dist/types/generated/shared/index.d.ts +0 -49
- package/dist/types/index.d.ts +0 -5
- package/dist/types/unwrap.d.ts +0 -45
- package/dist/types/utilities.d.ts +0 -39
- package/dist/types/wrap.d.ts +0 -42
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export * from './generated';
|
|
2
|
+
|
|
3
|
+
export { createMint, type CreateMintArgs, type CreateMintResult } from './create-mint';
|
|
4
|
+
export {
|
|
5
|
+
singleSignerWrap,
|
|
6
|
+
type SingleSignerWrapArgs,
|
|
7
|
+
type SingleSignerWrapResult,
|
|
8
|
+
multisigOfflineSignWrap,
|
|
9
|
+
type MultiSignerWrapIxBuilderArgs,
|
|
10
|
+
} from './wrap';
|
|
11
|
+
export {
|
|
12
|
+
singleSignerUnwrap,
|
|
13
|
+
type SingleSignerUnwrapArgs,
|
|
14
|
+
type SingleSignerUnwrapResult,
|
|
15
|
+
multisigOfflineSignUnwrap,
|
|
16
|
+
} from './unwrap';
|
|
17
|
+
export {
|
|
18
|
+
createEscrowAccount,
|
|
19
|
+
type CreateEscrowAccountArgs,
|
|
20
|
+
type CreateEscrowAccountResult,
|
|
21
|
+
combinedMultisigTx,
|
|
22
|
+
type MultiSigCombineArgs,
|
|
23
|
+
} from './utilities';
|
package/src/unwrap.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { findAssociatedTokenPda, getTokenDecoder } from '@solana-program/token-2022';
|
|
2
|
+
import {
|
|
3
|
+
Address,
|
|
4
|
+
appendTransactionMessageInstructions,
|
|
5
|
+
Blockhash,
|
|
6
|
+
createTransactionMessage,
|
|
7
|
+
fetchEncodedAccount,
|
|
8
|
+
GetAccountInfoApi,
|
|
9
|
+
Instruction,
|
|
10
|
+
pipe,
|
|
11
|
+
Rpc,
|
|
12
|
+
setTransactionMessageFeePayerSigner,
|
|
13
|
+
setTransactionMessageLifetimeUsingBlockhash,
|
|
14
|
+
TransactionMessage,
|
|
15
|
+
TransactionMessageWithBlockhashLifetime,
|
|
16
|
+
TransactionMessageWithFeePayerSigner,
|
|
17
|
+
TransactionSigner,
|
|
18
|
+
} from '@solana/kit';
|
|
19
|
+
import { findWrappedMintAuthorityPda, getUnwrapInstruction, UnwrapInput } from './generated';
|
|
20
|
+
import { getMintFromTokenAccount, getOwnerFromAccount } from './utilities';
|
|
21
|
+
|
|
22
|
+
export interface SingleSignerUnwrapArgs {
|
|
23
|
+
rpc: Rpc<GetAccountInfoApi>;
|
|
24
|
+
payer: TransactionSigner; // Fee payer and default transfer authority
|
|
25
|
+
wrappedTokenAccount: Address;
|
|
26
|
+
amount: bigint | number;
|
|
27
|
+
recipientUnwrappedToken: Address;
|
|
28
|
+
// Optional arguments below (will be derived/defaulted if not provided)
|
|
29
|
+
transferAuthority?: Address | TransactionSigner; // Defaults to payer
|
|
30
|
+
unwrappedMint?: Address; // Will derive from unwrappedEscrow if not provided
|
|
31
|
+
wrappedTokenProgram?: Address; // Will derive from wrappedTokenAccount if not provided
|
|
32
|
+
unwrappedTokenProgram?: Address; // Will derive from unwrappedEscrow if not provided
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function resolveUnwrapAddrs({
|
|
36
|
+
rpc,
|
|
37
|
+
payer,
|
|
38
|
+
wrappedTokenAccount,
|
|
39
|
+
recipientUnwrappedToken,
|
|
40
|
+
inputUnwrappedMint,
|
|
41
|
+
inputTransferAuthority,
|
|
42
|
+
inputWrappedTokenProgram,
|
|
43
|
+
inputUnwrappedTokenProgram,
|
|
44
|
+
}: {
|
|
45
|
+
rpc: Rpc<GetAccountInfoApi>;
|
|
46
|
+
payer: TransactionSigner;
|
|
47
|
+
wrappedTokenAccount: Address;
|
|
48
|
+
recipientUnwrappedToken: Address;
|
|
49
|
+
inputUnwrappedMint?: Address;
|
|
50
|
+
inputTransferAuthority?: Address | TransactionSigner;
|
|
51
|
+
inputWrappedTokenProgram?: Address;
|
|
52
|
+
inputUnwrappedTokenProgram?: Address;
|
|
53
|
+
}) {
|
|
54
|
+
const wrappedTokenProgram = inputWrappedTokenProgram ?? (await getOwnerFromAccount(rpc, wrappedTokenAccount));
|
|
55
|
+
const unwrappedTokenProgram =
|
|
56
|
+
inputUnwrappedTokenProgram ?? (await getOwnerFromAccount(rpc, recipientUnwrappedToken));
|
|
57
|
+
const unwrappedMint = inputUnwrappedMint ?? (await getMintFromTokenAccount(rpc, recipientUnwrappedToken));
|
|
58
|
+
|
|
59
|
+
// Get wrapped mint from the token account being burned
|
|
60
|
+
const wrappedAccountInfo = await fetchEncodedAccount(rpc, wrappedTokenAccount);
|
|
61
|
+
if (!wrappedAccountInfo.exists) {
|
|
62
|
+
throw new Error(`Wrapped token account ${wrappedTokenAccount} not found.`);
|
|
63
|
+
}
|
|
64
|
+
const wrappedMint = getTokenDecoder().decode(wrappedAccountInfo.data).mint;
|
|
65
|
+
|
|
66
|
+
const [wrappedMintAuthority] = await findWrappedMintAuthorityPda({ wrappedMint });
|
|
67
|
+
|
|
68
|
+
// Default transfer authority to payer if not provided
|
|
69
|
+
const transferAuthority = inputTransferAuthority ?? payer;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
unwrappedMint,
|
|
73
|
+
wrappedMint,
|
|
74
|
+
wrappedMintAuthority,
|
|
75
|
+
transferAuthority,
|
|
76
|
+
wrappedTokenProgram,
|
|
77
|
+
unwrappedTokenProgram,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface UnwrapTxBuilderArgs {
|
|
82
|
+
wrappedTokenAccount: Address;
|
|
83
|
+
amount: bigint | number;
|
|
84
|
+
wrappedMint: Address;
|
|
85
|
+
wrappedMintAuthority: Address;
|
|
86
|
+
unwrappedMint: Address;
|
|
87
|
+
recipientUnwrappedToken: Address;
|
|
88
|
+
unwrappedTokenProgram: Address;
|
|
89
|
+
wrappedTokenProgram: Address;
|
|
90
|
+
transferAuthority: Address | TransactionSigner;
|
|
91
|
+
multiSigners?: TransactionSigner[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function buildUnwrapTransaction({
|
|
95
|
+
recipientUnwrappedToken,
|
|
96
|
+
wrappedMintAuthority,
|
|
97
|
+
unwrappedMint,
|
|
98
|
+
wrappedTokenProgram,
|
|
99
|
+
unwrappedTokenProgram,
|
|
100
|
+
wrappedTokenAccount,
|
|
101
|
+
wrappedMint,
|
|
102
|
+
transferAuthority,
|
|
103
|
+
amount,
|
|
104
|
+
multiSigners = [],
|
|
105
|
+
}: UnwrapTxBuilderArgs): Promise<Instruction> {
|
|
106
|
+
const [unwrappedEscrow] = await findAssociatedTokenPda({
|
|
107
|
+
owner: wrappedMintAuthority,
|
|
108
|
+
mint: unwrappedMint,
|
|
109
|
+
tokenProgram: unwrappedTokenProgram,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const unwrapInstructionInput: UnwrapInput = {
|
|
113
|
+
unwrappedEscrow,
|
|
114
|
+
recipientUnwrappedToken,
|
|
115
|
+
wrappedMintAuthority,
|
|
116
|
+
unwrappedMint,
|
|
117
|
+
wrappedTokenProgram,
|
|
118
|
+
unwrappedTokenProgram,
|
|
119
|
+
wrappedTokenAccount,
|
|
120
|
+
wrappedMint,
|
|
121
|
+
transferAuthority,
|
|
122
|
+
amount: BigInt(amount),
|
|
123
|
+
multiSigners,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return getUnwrapInstruction(unwrapInstructionInput);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface SingleSignerUnwrapResult {
|
|
130
|
+
ixs: Instruction[];
|
|
131
|
+
recipientUnwrappedToken: Address;
|
|
132
|
+
amount: bigint;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Creates, signs (single signer or default authority), and sends an unwrap transaction.
|
|
137
|
+
* Derives necessary PDAs and default accounts if not provided.
|
|
138
|
+
*/
|
|
139
|
+
export async function singleSignerUnwrap({
|
|
140
|
+
rpc,
|
|
141
|
+
payer,
|
|
142
|
+
wrappedTokenAccount,
|
|
143
|
+
amount,
|
|
144
|
+
recipientUnwrappedToken,
|
|
145
|
+
transferAuthority: inputTransferAuthority,
|
|
146
|
+
unwrappedMint: inputUnwrappedMint,
|
|
147
|
+
wrappedTokenProgram: inputWrappedTokenProgram,
|
|
148
|
+
unwrappedTokenProgram: inputUnwrappedTokenProgram,
|
|
149
|
+
}: SingleSignerUnwrapArgs): Promise<SingleSignerUnwrapResult> {
|
|
150
|
+
const {
|
|
151
|
+
wrappedMint,
|
|
152
|
+
wrappedMintAuthority,
|
|
153
|
+
transferAuthority,
|
|
154
|
+
unwrappedTokenProgram,
|
|
155
|
+
unwrappedMint,
|
|
156
|
+
wrappedTokenProgram,
|
|
157
|
+
} = await resolveUnwrapAddrs({
|
|
158
|
+
rpc,
|
|
159
|
+
payer,
|
|
160
|
+
wrappedTokenAccount,
|
|
161
|
+
recipientUnwrappedToken,
|
|
162
|
+
inputUnwrappedMint,
|
|
163
|
+
inputTransferAuthority,
|
|
164
|
+
inputWrappedTokenProgram,
|
|
165
|
+
inputUnwrappedTokenProgram,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const ix = await buildUnwrapTransaction({
|
|
169
|
+
recipientUnwrappedToken,
|
|
170
|
+
wrappedMintAuthority,
|
|
171
|
+
unwrappedMint,
|
|
172
|
+
wrappedTokenProgram,
|
|
173
|
+
unwrappedTokenProgram,
|
|
174
|
+
wrappedTokenAccount,
|
|
175
|
+
wrappedMint,
|
|
176
|
+
transferAuthority,
|
|
177
|
+
amount,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
recipientUnwrappedToken,
|
|
182
|
+
amount: BigInt(amount),
|
|
183
|
+
ixs: [ix],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface MultiSignerUnWrapTxBuilderArgs extends UnwrapTxBuilderArgs {
|
|
188
|
+
payer: TransactionSigner;
|
|
189
|
+
blockhash: {
|
|
190
|
+
blockhash: Blockhash;
|
|
191
|
+
lastValidBlockHeight: bigint;
|
|
192
|
+
};
|
|
193
|
+
multiSigners: TransactionSigner[];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Used to collect signatures
|
|
197
|
+
export async function multisigOfflineSignUnwrap(
|
|
198
|
+
args: MultiSignerUnWrapTxBuilderArgs,
|
|
199
|
+
): Promise<TransactionMessage & TransactionMessageWithBlockhashLifetime & TransactionMessageWithFeePayerSigner> {
|
|
200
|
+
const unwrapIx = await buildUnwrapTransaction(args);
|
|
201
|
+
|
|
202
|
+
return pipe(
|
|
203
|
+
createTransactionMessage({ version: 0 }),
|
|
204
|
+
tx => setTransactionMessageFeePayerSigner(args.payer, tx),
|
|
205
|
+
tx => setTransactionMessageLifetimeUsingBlockhash(args.blockhash, tx),
|
|
206
|
+
tx => appendTransactionMessageInstructions([unwrapIx], tx),
|
|
207
|
+
);
|
|
208
|
+
}
|
package/src/utilities.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { getCreateAccountInstruction } from '@solana-program/system';
|
|
2
|
+
import { getInitializeAccountInstruction as initializeToken, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
|
|
3
|
+
import {
|
|
4
|
+
fetchMaybeToken,
|
|
5
|
+
findAssociatedTokenPda,
|
|
6
|
+
getCreateAssociatedTokenInstruction,
|
|
7
|
+
getTokenDecoder,
|
|
8
|
+
InitializeAccountInput,
|
|
9
|
+
getInitializeAccountInstruction as initializeToken2022,
|
|
10
|
+
Token,
|
|
11
|
+
TOKEN_2022_PROGRAM_ADDRESS,
|
|
12
|
+
} from '@solana-program/token-2022';
|
|
13
|
+
import {
|
|
14
|
+
Account,
|
|
15
|
+
Address,
|
|
16
|
+
assertIsFullySignedTransaction,
|
|
17
|
+
assertIsSendableTransaction,
|
|
18
|
+
Blockhash,
|
|
19
|
+
containsBytes,
|
|
20
|
+
fetchEncodedAccount,
|
|
21
|
+
FullySignedTransaction,
|
|
22
|
+
generateKeyPairSigner,
|
|
23
|
+
GetAccountInfoApi,
|
|
24
|
+
GetMinimumBalanceForRentExemptionApi,
|
|
25
|
+
Instruction,
|
|
26
|
+
KeyPairSigner,
|
|
27
|
+
Rpc,
|
|
28
|
+
SignatureBytes,
|
|
29
|
+
Transaction,
|
|
30
|
+
TransactionWithBlockhashLifetime,
|
|
31
|
+
TransactionWithinSizeLimit,
|
|
32
|
+
} from '@solana/kit';
|
|
33
|
+
import { findWrappedMintAuthorityPda, findWrappedMintPda } from './generated';
|
|
34
|
+
|
|
35
|
+
function getInitializeTokenFn(tokenProgram: Address): (input: InitializeAccountInput) => Instruction {
|
|
36
|
+
if (tokenProgram === TOKEN_PROGRAM_ADDRESS) return initializeToken;
|
|
37
|
+
if (tokenProgram === TOKEN_2022_PROGRAM_ADDRESS) return initializeToken2022;
|
|
38
|
+
throw new Error(`${tokenProgram} is not a valid token program.`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function createTokenAccount({
|
|
42
|
+
rpc,
|
|
43
|
+
payer,
|
|
44
|
+
mint,
|
|
45
|
+
owner,
|
|
46
|
+
tokenProgram,
|
|
47
|
+
}: {
|
|
48
|
+
rpc: Rpc<GetMinimumBalanceForRentExemptionApi>;
|
|
49
|
+
payer: KeyPairSigner;
|
|
50
|
+
mint: Address;
|
|
51
|
+
owner: Address;
|
|
52
|
+
tokenProgram: Address;
|
|
53
|
+
}): Promise<{ ixs: Instruction[]; keyPair: KeyPairSigner }> {
|
|
54
|
+
const [keyPair, lamports] = await Promise.all([
|
|
55
|
+
generateKeyPairSigner(),
|
|
56
|
+
rpc.getMinimumBalanceForRentExemption(165n).send(),
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const createAccountIx = getCreateAccountInstruction({
|
|
60
|
+
payer,
|
|
61
|
+
newAccount: keyPair,
|
|
62
|
+
lamports,
|
|
63
|
+
space: 165,
|
|
64
|
+
programAddress: tokenProgram,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const initializeAccountIx = getInitializeTokenFn(tokenProgram)({
|
|
68
|
+
account: keyPair.address,
|
|
69
|
+
mint,
|
|
70
|
+
owner,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
ixs: [createAccountIx, initializeAccountIx],
|
|
75
|
+
keyPair,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CreateEscrowAccountArgs {
|
|
80
|
+
rpc: Rpc<GetAccountInfoApi & GetMinimumBalanceForRentExemptionApi>;
|
|
81
|
+
payer: KeyPairSigner;
|
|
82
|
+
unwrappedMint: Address;
|
|
83
|
+
wrappedTokenProgram: Address;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type CreateEscrowAccountResult =
|
|
87
|
+
| { kind: 'already_exists'; account: Account<Token> }
|
|
88
|
+
| {
|
|
89
|
+
kind: 'instructions_to_create';
|
|
90
|
+
address: Address;
|
|
91
|
+
ixs: Instruction[];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export async function createEscrowAccount({
|
|
95
|
+
rpc,
|
|
96
|
+
payer,
|
|
97
|
+
unwrappedMint,
|
|
98
|
+
wrappedTokenProgram,
|
|
99
|
+
}: CreateEscrowAccountArgs): Promise<CreateEscrowAccountResult> {
|
|
100
|
+
const [wrappedMint] = await findWrappedMintPda({ unwrappedMint, wrappedTokenProgram });
|
|
101
|
+
const [wrappedMintAuthority] = await findWrappedMintAuthorityPda({ wrappedMint });
|
|
102
|
+
const unwrappedTokenProgram = await getOwnerFromAccount(rpc, unwrappedMint);
|
|
103
|
+
|
|
104
|
+
const [escrowAta] = await findAssociatedTokenPda({
|
|
105
|
+
owner: wrappedMintAuthority,
|
|
106
|
+
mint: unwrappedMint,
|
|
107
|
+
tokenProgram: unwrappedTokenProgram,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const escrowResult = await fetchMaybeToken(rpc, escrowAta);
|
|
111
|
+
if (escrowResult.exists) {
|
|
112
|
+
return { kind: 'already_exists', account: escrowResult };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const ix = getCreateAssociatedTokenInstruction({
|
|
116
|
+
payer,
|
|
117
|
+
owner: wrappedMintAuthority,
|
|
118
|
+
mint: unwrappedMint,
|
|
119
|
+
ata: escrowAta,
|
|
120
|
+
tokenProgram: unwrappedTokenProgram,
|
|
121
|
+
}) as Instruction;
|
|
122
|
+
|
|
123
|
+
return { address: escrowAta, ixs: [ix], kind: 'instructions_to_create' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function getOwnerFromAccount(rpc: Rpc<GetAccountInfoApi>, accountAddress: Address): Promise<Address> {
|
|
127
|
+
const accountInfo = await rpc.getAccountInfo(accountAddress, { encoding: 'base64' }).send();
|
|
128
|
+
if (!accountInfo.value) {
|
|
129
|
+
throw new Error(`Account ${accountAddress} not found.`);
|
|
130
|
+
}
|
|
131
|
+
return accountInfo.value.owner;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function getMintFromTokenAccount(
|
|
135
|
+
rpc: Rpc<GetAccountInfoApi>,
|
|
136
|
+
tokenAccountAddress: Address,
|
|
137
|
+
): Promise<Address> {
|
|
138
|
+
const account = await fetchEncodedAccount(rpc, tokenAccountAddress);
|
|
139
|
+
if (!account.exists) {
|
|
140
|
+
throw new Error(`Unwrapped token account ${tokenAccountAddress} not found.`);
|
|
141
|
+
}
|
|
142
|
+
return getTokenDecoder().decode(account.data).mint;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function messageBytesEqual(results: (Transaction & TransactionWithBlockhashLifetime)[]): boolean {
|
|
146
|
+
// If array has only one element, return true
|
|
147
|
+
if (results.length === 1) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Use the first result as reference
|
|
152
|
+
const reference = results[0];
|
|
153
|
+
if (!reference) throw new Error('No transactions in input');
|
|
154
|
+
|
|
155
|
+
// Compare each result with the reference
|
|
156
|
+
return results.every(
|
|
157
|
+
c =>
|
|
158
|
+
reference.messageBytes.length === c.messageBytes.length &&
|
|
159
|
+
containsBytes(reference.messageBytes, c.messageBytes, 0),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function combineSignatures(
|
|
164
|
+
signedTxs: (Transaction & TransactionWithBlockhashLifetime)[],
|
|
165
|
+
): Record<string, SignatureBytes> {
|
|
166
|
+
// Step 1: Determine the canonical signer order from the first signed transaction.
|
|
167
|
+
// Insertion order is the way to re-create this. Without it, verification will fail.
|
|
168
|
+
const firstSignedTx = signedTxs[0];
|
|
169
|
+
if (!firstSignedTx) {
|
|
170
|
+
throw new Error('No signed transactions provided');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const allSignatures: Record<string, SignatureBytes | null> = {};
|
|
174
|
+
|
|
175
|
+
// Step 1: Insert a null signature for each signer, maintaining the order of the signatures from the first signed transaction
|
|
176
|
+
for (const pubkey of Object.keys(firstSignedTx.signatures)) {
|
|
177
|
+
allSignatures[pubkey] = null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Step 2: Gather all signatures from all transactions
|
|
181
|
+
for (const signedTx of signedTxs) {
|
|
182
|
+
for (const [address, signature] of Object.entries(signedTx.signatures)) {
|
|
183
|
+
if (signature) {
|
|
184
|
+
// only store non-null signers
|
|
185
|
+
allSignatures[address] = signature;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Step 3: Assert all signatures are set
|
|
191
|
+
const missingSigners: string[] = [];
|
|
192
|
+
for (const [pubkey, signature] of Object.entries(allSignatures)) {
|
|
193
|
+
if (signature === null) {
|
|
194
|
+
missingSigners.push(pubkey);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (missingSigners.length > 0) {
|
|
198
|
+
throw new Error(`Missing signatures for: ${missingSigners.join(', ')}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return allSignatures as Record<string, SignatureBytes>;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface MultiSigCombineArgs {
|
|
205
|
+
signedTxs: (Transaction & TransactionWithBlockhashLifetime)[];
|
|
206
|
+
blockhash: {
|
|
207
|
+
blockhash: Blockhash;
|
|
208
|
+
lastValidBlockHeight: bigint;
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Combines, validates, and broadcasts outputs of multisig offline partially signed txs
|
|
213
|
+
export function combinedMultisigTx({
|
|
214
|
+
signedTxs,
|
|
215
|
+
blockhash,
|
|
216
|
+
}: MultiSigCombineArgs): Transaction &
|
|
217
|
+
FullySignedTransaction &
|
|
218
|
+
TransactionWithBlockhashLifetime &
|
|
219
|
+
TransactionWithinSizeLimit {
|
|
220
|
+
const messagesEqual = messageBytesEqual(signedTxs);
|
|
221
|
+
if (!messagesEqual) throw new Error('Messages are not all the same');
|
|
222
|
+
if (!signedTxs[0]) throw new Error('No signed transactions provided');
|
|
223
|
+
|
|
224
|
+
const tx = {
|
|
225
|
+
messageBytes: signedTxs[0].messageBytes,
|
|
226
|
+
signatures: combineSignatures(signedTxs),
|
|
227
|
+
lifetimeConstraint: blockhash,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
assertIsFullySignedTransaction(tx);
|
|
231
|
+
assertIsSendableTransaction(tx);
|
|
232
|
+
|
|
233
|
+
return tx;
|
|
234
|
+
}
|
package/src/wrap.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { findAssociatedTokenPda } from '@solana-program/token-2022';
|
|
2
|
+
import {
|
|
3
|
+
Address,
|
|
4
|
+
appendTransactionMessageInstructions,
|
|
5
|
+
Blockhash,
|
|
6
|
+
createTransactionMessage,
|
|
7
|
+
GetAccountInfoApi,
|
|
8
|
+
Instruction,
|
|
9
|
+
pipe,
|
|
10
|
+
Rpc,
|
|
11
|
+
setTransactionMessageFeePayerSigner,
|
|
12
|
+
setTransactionMessageLifetimeUsingBlockhash,
|
|
13
|
+
TransactionMessage,
|
|
14
|
+
TransactionMessageWithBlockhashLifetime,
|
|
15
|
+
TransactionMessageWithFeePayerSigner,
|
|
16
|
+
TransactionSigner,
|
|
17
|
+
} from '@solana/kit';
|
|
18
|
+
import { findWrappedMintAuthorityPda, findWrappedMintPda, getWrapInstruction, WrapInput } from './generated';
|
|
19
|
+
import { getMintFromTokenAccount, getOwnerFromAccount } from './utilities';
|
|
20
|
+
|
|
21
|
+
interface IxBuilderArgs {
|
|
22
|
+
unwrappedTokenAccount: Address;
|
|
23
|
+
wrappedTokenProgram: Address;
|
|
24
|
+
amount: bigint | number;
|
|
25
|
+
wrappedMint: Address;
|
|
26
|
+
wrappedMintAuthority: Address;
|
|
27
|
+
transferAuthority: Address | TransactionSigner;
|
|
28
|
+
unwrappedMint: Address;
|
|
29
|
+
recipientWrappedTokenAccount: Address;
|
|
30
|
+
unwrappedTokenProgram: Address;
|
|
31
|
+
multiSigners?: TransactionSigner[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface MultiSignerWrapIxBuilderArgs extends IxBuilderArgs {
|
|
35
|
+
payer: TransactionSigner;
|
|
36
|
+
blockhash: {
|
|
37
|
+
blockhash: Blockhash;
|
|
38
|
+
lastValidBlockHeight: bigint;
|
|
39
|
+
};
|
|
40
|
+
multiSigners: TransactionSigner[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Used to collect signatures
|
|
44
|
+
export async function multisigOfflineSignWrap(
|
|
45
|
+
args: MultiSignerWrapIxBuilderArgs,
|
|
46
|
+
): Promise<TransactionMessage & TransactionMessageWithBlockhashLifetime & TransactionMessageWithFeePayerSigner> {
|
|
47
|
+
const wrapIx = await buildWrapIx(args);
|
|
48
|
+
|
|
49
|
+
return pipe(
|
|
50
|
+
createTransactionMessage({ version: 0 }),
|
|
51
|
+
tx => setTransactionMessageFeePayerSigner(args.payer, tx),
|
|
52
|
+
tx => setTransactionMessageLifetimeUsingBlockhash(args.blockhash, tx),
|
|
53
|
+
tx => appendTransactionMessageInstructions([wrapIx], tx),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SingleSignerWrapArgs {
|
|
58
|
+
rpc: Rpc<GetAccountInfoApi>;
|
|
59
|
+
payer: TransactionSigner; // Fee payer and default transfer authority
|
|
60
|
+
unwrappedTokenAccount: Address;
|
|
61
|
+
wrappedTokenProgram: Address;
|
|
62
|
+
amount: bigint | number;
|
|
63
|
+
transferAuthority?: Address | TransactionSigner; // Defaults to payer if not provided
|
|
64
|
+
unwrappedMint?: Address; // Will fetch from unwrappedTokenAccount if not provided
|
|
65
|
+
recipientWrappedTokenAccount?: Address; // Defaults to payer's ATA if not provided
|
|
66
|
+
unwrappedTokenProgram?: Address; // Will fetch from unwrappedTokenAccount owner if not provided
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SingleSignerWrapResult {
|
|
70
|
+
ixs: Instruction[];
|
|
71
|
+
recipientWrappedTokenAccount: Address;
|
|
72
|
+
escrowAccount: Address;
|
|
73
|
+
amount: bigint;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function singleSignerWrap({
|
|
77
|
+
rpc,
|
|
78
|
+
payer,
|
|
79
|
+
unwrappedTokenAccount,
|
|
80
|
+
wrappedTokenProgram,
|
|
81
|
+
amount,
|
|
82
|
+
transferAuthority: inputTransferAuthority,
|
|
83
|
+
unwrappedMint: inputUnwrappedMint,
|
|
84
|
+
recipientWrappedTokenAccount: inputRecipientTokenAccount,
|
|
85
|
+
unwrappedTokenProgram: inputUnwrappedTokenProgram,
|
|
86
|
+
}: SingleSignerWrapArgs): Promise<SingleSignerWrapResult> {
|
|
87
|
+
const {
|
|
88
|
+
unwrappedMint,
|
|
89
|
+
unwrappedTokenProgram,
|
|
90
|
+
wrappedMint,
|
|
91
|
+
wrappedMintAuthority,
|
|
92
|
+
recipientWrappedTokenAccount,
|
|
93
|
+
transferAuthority,
|
|
94
|
+
unwrappedEscrow,
|
|
95
|
+
} = await resolveAddrs({
|
|
96
|
+
rpc,
|
|
97
|
+
payer,
|
|
98
|
+
inputTransferAuthority,
|
|
99
|
+
inputUnwrappedMint,
|
|
100
|
+
unwrappedTokenAccount,
|
|
101
|
+
inputUnwrappedTokenProgram,
|
|
102
|
+
wrappedTokenProgram,
|
|
103
|
+
inputRecipientTokenAccount,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const ix = await buildWrapIx({
|
|
107
|
+
unwrappedTokenAccount,
|
|
108
|
+
wrappedTokenProgram,
|
|
109
|
+
amount,
|
|
110
|
+
transferAuthority,
|
|
111
|
+
unwrappedMint,
|
|
112
|
+
wrappedMint,
|
|
113
|
+
wrappedMintAuthority,
|
|
114
|
+
recipientWrappedTokenAccount,
|
|
115
|
+
unwrappedTokenProgram,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
ixs: [ix],
|
|
120
|
+
recipientWrappedTokenAccount,
|
|
121
|
+
escrowAccount: unwrappedEscrow,
|
|
122
|
+
amount: BigInt(amount),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Meant to handle all of the potential default values
|
|
127
|
+
async function resolveAddrs({
|
|
128
|
+
rpc,
|
|
129
|
+
payer,
|
|
130
|
+
unwrappedTokenAccount,
|
|
131
|
+
wrappedTokenProgram,
|
|
132
|
+
inputTransferAuthority,
|
|
133
|
+
inputUnwrappedMint,
|
|
134
|
+
inputRecipientTokenAccount,
|
|
135
|
+
inputUnwrappedTokenProgram,
|
|
136
|
+
}: {
|
|
137
|
+
rpc: Rpc<GetAccountInfoApi>;
|
|
138
|
+
payer: TransactionSigner;
|
|
139
|
+
unwrappedTokenAccount: Address;
|
|
140
|
+
wrappedTokenProgram: Address;
|
|
141
|
+
inputTransferAuthority?: Address | TransactionSigner;
|
|
142
|
+
inputUnwrappedMint?: Address;
|
|
143
|
+
inputRecipientTokenAccount?: Address;
|
|
144
|
+
inputUnwrappedTokenProgram?: Address;
|
|
145
|
+
}) {
|
|
146
|
+
const unwrappedMint = inputUnwrappedMint ?? (await getMintFromTokenAccount(rpc, unwrappedTokenAccount));
|
|
147
|
+
const unwrappedTokenProgram = inputUnwrappedTokenProgram ?? (await getOwnerFromAccount(rpc, unwrappedTokenAccount));
|
|
148
|
+
const [wrappedMint] = await findWrappedMintPda({ unwrappedMint, wrappedTokenProgram });
|
|
149
|
+
const [wrappedMintAuthority] = await findWrappedMintAuthorityPda({ wrappedMint });
|
|
150
|
+
const recipientWrappedTokenAccount =
|
|
151
|
+
inputRecipientTokenAccount ??
|
|
152
|
+
(
|
|
153
|
+
await findAssociatedTokenPda({
|
|
154
|
+
owner: payer.address,
|
|
155
|
+
mint: wrappedMint,
|
|
156
|
+
tokenProgram: wrappedTokenProgram,
|
|
157
|
+
})
|
|
158
|
+
)[0];
|
|
159
|
+
const [unwrappedEscrow] = await findAssociatedTokenPda({
|
|
160
|
+
owner: wrappedMintAuthority,
|
|
161
|
+
mint: unwrappedMint,
|
|
162
|
+
tokenProgram: unwrappedTokenProgram,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const transferAuthority = inputTransferAuthority ?? payer;
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
unwrappedEscrow,
|
|
169
|
+
transferAuthority,
|
|
170
|
+
unwrappedMint,
|
|
171
|
+
unwrappedTokenProgram,
|
|
172
|
+
wrappedMint,
|
|
173
|
+
wrappedMintAuthority,
|
|
174
|
+
recipientWrappedTokenAccount,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function buildWrapIx({
|
|
179
|
+
unwrappedTokenAccount,
|
|
180
|
+
wrappedTokenProgram,
|
|
181
|
+
amount,
|
|
182
|
+
transferAuthority,
|
|
183
|
+
unwrappedMint,
|
|
184
|
+
recipientWrappedTokenAccount,
|
|
185
|
+
unwrappedTokenProgram,
|
|
186
|
+
wrappedMint,
|
|
187
|
+
wrappedMintAuthority,
|
|
188
|
+
multiSigners = [],
|
|
189
|
+
}: IxBuilderArgs): Promise<Instruction> {
|
|
190
|
+
const [unwrappedEscrow] = await findAssociatedTokenPda({
|
|
191
|
+
owner: wrappedMintAuthority,
|
|
192
|
+
mint: unwrappedMint,
|
|
193
|
+
tokenProgram: unwrappedTokenProgram,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const wrapInstructionInput: WrapInput = {
|
|
197
|
+
recipientWrappedTokenAccount,
|
|
198
|
+
wrappedMint,
|
|
199
|
+
wrappedMintAuthority,
|
|
200
|
+
unwrappedTokenProgram,
|
|
201
|
+
wrappedTokenProgram,
|
|
202
|
+
unwrappedTokenAccount,
|
|
203
|
+
unwrappedMint,
|
|
204
|
+
unwrappedEscrow,
|
|
205
|
+
transferAuthority,
|
|
206
|
+
amount: BigInt(amount),
|
|
207
|
+
multiSigners,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return getWrapInstruction(wrapInstructionInput);
|
|
211
|
+
}
|