@exodus/solana-lib 2.0.0 → 2.2.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 +7 -4
- package/src/constants.js +2 -0
- package/src/encode.js +8 -2
- package/src/helpers/metaplex-transfer.js +6 -3
- package/src/helpers/spl-token-2022.js +204 -0
- package/src/helpers/tokenTransfer.js +9 -5
- package/src/transaction.js +41 -10
- package/src/tx/async-account.js +38 -0
- package/src/tx/create-unsigned-tx.js +2 -0
- package/src/tx/sign-unsigned-tx.js +44 -25
- package/src/vendor/transaction.js +18 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-lib",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Exodus internal Solana low-level library",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -33,8 +33,11 @@
|
|
|
33
33
|
"tweetnacl": "^1.0.3"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@exodus/
|
|
37
|
-
"@
|
|
36
|
+
"@exodus/key-identifier": "1.0.0",
|
|
37
|
+
"@exodus/keychain": "^6.2.0",
|
|
38
|
+
"@exodus/solana-meta": "^1.1.0",
|
|
39
|
+
"@solana/web3.js": "^1.90.0",
|
|
40
|
+
"bip39": "^2.6.0"
|
|
38
41
|
},
|
|
39
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "85fd61e1f2f77aff4c32b5684eb5f4845f06794a"
|
|
40
43
|
}
|
package/src/constants.js
CHANGED
|
@@ -9,6 +9,8 @@ export const STAKE_PROGRAM_ID = StakeProgram.programId
|
|
|
9
9
|
|
|
10
10
|
export const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA')
|
|
11
11
|
|
|
12
|
+
export const TOKEN_2022_PROGRAM_ID = new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb')
|
|
13
|
+
|
|
12
14
|
export const MEMO_PROGRAM_ID = new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr')
|
|
13
15
|
|
|
14
16
|
export const MAGIC_EDEN_ESCROW_PROGRAM_ID = new PublicKey(
|
package/src/encode.js
CHANGED
|
@@ -47,11 +47,17 @@ export function getPrivateKeyFromSecretKey(secretKey) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// doc: https://spl.solana.com/associated-token-account (HACK: refactored to sync)
|
|
50
|
-
export function findAssociatedTokenAddress(
|
|
50
|
+
export function findAssociatedTokenAddress(
|
|
51
|
+
walletAddress,
|
|
52
|
+
tokenMintAddress,
|
|
53
|
+
programId = TOKEN_PROGRAM_ID.toBase58() // or TOKEN_2022_PROGRAM_ID
|
|
54
|
+
) {
|
|
51
55
|
walletAddress = new PublicKey(walletAddress)
|
|
52
56
|
tokenMintAddress = new PublicKey(tokenMintAddress)
|
|
57
|
+
programId = programId instanceof PublicKey ? programId : new PublicKey(programId)
|
|
58
|
+
|
|
53
59
|
return PublicKey.findProgramAddress(
|
|
54
|
-
[walletAddress.toBuffer(),
|
|
60
|
+
[walletAddress.toBuffer(), programId.toBuffer(), tokenMintAddress.toBuffer()],
|
|
55
61
|
SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID
|
|
56
62
|
)[0].toBase58() // returns encoded PublicKey
|
|
57
63
|
}
|
|
@@ -52,6 +52,7 @@ export const prepareMetaplexTransferTx = ({
|
|
|
52
52
|
amount,
|
|
53
53
|
authorityType,
|
|
54
54
|
programId = MPL_TOKEN_METADATA_PROGRAM_ID,
|
|
55
|
+
tokenProgram,
|
|
55
56
|
}) => {
|
|
56
57
|
const transaction = new Transaction()
|
|
57
58
|
const data = encodeData({ amount, authorityType })
|
|
@@ -122,7 +123,7 @@ export const prepareMetaplexTransferTx = ({
|
|
|
122
123
|
isSigner: false,
|
|
123
124
|
},
|
|
124
125
|
{
|
|
125
|
-
pubkey:
|
|
126
|
+
pubkey: tokenProgram,
|
|
126
127
|
isWritable: false,
|
|
127
128
|
isSigner: false,
|
|
128
129
|
},
|
|
@@ -161,10 +162,11 @@ export function createMetaplexTransferTransaction({
|
|
|
161
162
|
tokenStandard,
|
|
162
163
|
recentBlockhash,
|
|
163
164
|
amount = 1,
|
|
165
|
+
tokenProgram = TOKEN_PROGRAM_ID,
|
|
164
166
|
}) {
|
|
165
|
-
const fromAccount = findAssociatedTokenAddress(from, tokenMintAddress)
|
|
167
|
+
const fromAccount = findAssociatedTokenAddress(from, tokenMintAddress, tokenProgram.toBase58())
|
|
166
168
|
|
|
167
|
-
const toAccount = findAssociatedTokenAddress(to, tokenMintAddress)
|
|
169
|
+
const toAccount = findAssociatedTokenAddress(to, tokenMintAddress, tokenProgram.toBase58())
|
|
168
170
|
|
|
169
171
|
const metadata = getMetadataAccount(tokenMintAddress)
|
|
170
172
|
const edition = getMasterEditionPDA(tokenMintAddress)
|
|
@@ -186,6 +188,7 @@ export function createMetaplexTransferTransaction({
|
|
|
186
188
|
payer: new PublicKey(from),
|
|
187
189
|
authorizationRulesProgram: TOKEN_AUTH_RULES_ID,
|
|
188
190
|
amount,
|
|
191
|
+
tokenProgram,
|
|
189
192
|
})
|
|
190
193
|
|
|
191
194
|
transaction.recentBlockhash = recentBlockhash
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { PublicKey, TransactionInstruction } from '../vendor'
|
|
2
|
+
import { struct, u8 } from '@exodus/buffer-layout'
|
|
3
|
+
import * as BufferLayout from '@exodus/buffer-layout'
|
|
4
|
+
import { U64 } from './spl-token'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Layout for a 64bit unsigned value
|
|
8
|
+
*/
|
|
9
|
+
const u64 = (property = 'uint64') => {
|
|
10
|
+
return BufferLayout.blob(8, property)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Extracted from https://github.com/solana-labs/solana-program-library/blob/token-js-v0.4.1/token/js/src/extensions/transferFee/instructions.ts
|
|
14
|
+
|
|
15
|
+
export const TOKEN_2022_PROGRAM_ID = new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb')
|
|
16
|
+
|
|
17
|
+
export const TransferFeeInstruction = {
|
|
18
|
+
InitializeTransferFeeConfig: 0,
|
|
19
|
+
TransferCheckedWithFee: 1,
|
|
20
|
+
WithdrawWithheldTokensFromMint: 2,
|
|
21
|
+
WithdrawWithheldTokensFromAccounts: 3,
|
|
22
|
+
HarvestWithheldTokensToMint: 4,
|
|
23
|
+
SetTransferFee: 5,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// https://github.com/solana-labs/solana-program-library/blob/token-js-v0.4.1/token/js/src/instructions/types.ts
|
|
27
|
+
export const TokenInstruction = {
|
|
28
|
+
TransferFeeExtension: 26,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/*
|
|
32
|
+
// TransferCheckedWithFee
|
|
33
|
+
export interface TransferCheckedWithFeeInstructionData {
|
|
34
|
+
instruction: TokenInstruction.TransferFeeExtension;
|
|
35
|
+
transferFeeInstruction: TransferFeeInstruction.TransferCheckedWithFee;
|
|
36
|
+
amount: bigint;
|
|
37
|
+
decimals: number;
|
|
38
|
+
fee: bigint;
|
|
39
|
+
}
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export const transferCheckedWithFeeInstructionData = struct([
|
|
43
|
+
u8('instruction'),
|
|
44
|
+
u8('transferFeeInstruction'),
|
|
45
|
+
u64('amount'),
|
|
46
|
+
u8('decimals'),
|
|
47
|
+
u64('fee'),
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Construct an TransferCheckedWithFee instruction
|
|
52
|
+
*
|
|
53
|
+
* @param source The source account
|
|
54
|
+
* @param mint The token mint
|
|
55
|
+
* @param destination The destination account
|
|
56
|
+
* @param authority The source account's owner/delegate
|
|
57
|
+
* @param signers The signer account(s)
|
|
58
|
+
* @param amount The amount of tokens to transfer
|
|
59
|
+
* @param decimals The expected number of base 10 digits to the right of the decimal place
|
|
60
|
+
* @param fee The expected fee assesed on this transfer, calculated off-chain based on the transferFeeBasisPoints and maximumFee of the mint.
|
|
61
|
+
* @param programId SPL Token program account
|
|
62
|
+
*
|
|
63
|
+
* @return Instruction to add to a transaction
|
|
64
|
+
*/
|
|
65
|
+
export function createTransferCheckedWithFeeInstruction(
|
|
66
|
+
source, // PublicKey
|
|
67
|
+
mint, // PublicKey
|
|
68
|
+
destination, // PublicKey
|
|
69
|
+
authority, // PublicKey
|
|
70
|
+
amount, // bigint
|
|
71
|
+
decimals, // number
|
|
72
|
+
fee, // bigint
|
|
73
|
+
multiSigners = [], // (Signer | PublicKey)[]
|
|
74
|
+
programId = TOKEN_2022_PROGRAM_ID
|
|
75
|
+
) {
|
|
76
|
+
source = new PublicKey(source)
|
|
77
|
+
mint = new PublicKey(mint)
|
|
78
|
+
destination = new PublicKey(destination)
|
|
79
|
+
authority = new PublicKey(authority)
|
|
80
|
+
|
|
81
|
+
if (programId !== TOKEN_2022_PROGRAM_ID) {
|
|
82
|
+
throw new Error('TokenUnsupportedInstructionError')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const data = Buffer.alloc(transferCheckedWithFeeInstructionData.span)
|
|
86
|
+
transferCheckedWithFeeInstructionData.encode(
|
|
87
|
+
{
|
|
88
|
+
instruction: TokenInstruction.TransferFeeExtension,
|
|
89
|
+
transferFeeInstruction: TransferFeeInstruction.TransferCheckedWithFee,
|
|
90
|
+
amount: new U64(amount).toBuffer(),
|
|
91
|
+
decimals,
|
|
92
|
+
fee: new U64(fee).toBuffer(),
|
|
93
|
+
},
|
|
94
|
+
data
|
|
95
|
+
)
|
|
96
|
+
const keys = addSigners(
|
|
97
|
+
[
|
|
98
|
+
{ pubkey: source, isSigner: false, isWritable: true },
|
|
99
|
+
{ pubkey: mint, isSigner: false, isWritable: false },
|
|
100
|
+
{ pubkey: destination, isSigner: false, isWritable: true },
|
|
101
|
+
],
|
|
102
|
+
authority,
|
|
103
|
+
multiSigners
|
|
104
|
+
)
|
|
105
|
+
return new TransactionInstruction({ keys, programId, data })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Decode a TransferCheckedWithFee instruction and validate it
|
|
110
|
+
*
|
|
111
|
+
* @param instruction Transaction instruction to decode
|
|
112
|
+
* @param programId SPL Token program account
|
|
113
|
+
*
|
|
114
|
+
* @return Decoded, valid instruction
|
|
115
|
+
*/
|
|
116
|
+
export function decodeTransferCheckedWithFeeInstruction(
|
|
117
|
+
instruction, // TransactionInstruction
|
|
118
|
+
programId // PublicKey
|
|
119
|
+
) {
|
|
120
|
+
if (!instruction.programId.equals(programId))
|
|
121
|
+
throw new Error('TokenInvalidInstructionProgramError')
|
|
122
|
+
if (instruction.data.length !== transferCheckedWithFeeInstructionData.span)
|
|
123
|
+
throw new Error('TokenInvalidInstructionDataError')
|
|
124
|
+
|
|
125
|
+
const {
|
|
126
|
+
keys: { source, mint, destination, authority, signers },
|
|
127
|
+
data,
|
|
128
|
+
} = decodeTransferCheckedWithFeeInstructionUnchecked(instruction)
|
|
129
|
+
if (
|
|
130
|
+
data.instruction !== TokenInstruction.TransferFeeExtension ||
|
|
131
|
+
data.transferFeeInstruction !== TransferFeeInstruction.TransferCheckedWithFee
|
|
132
|
+
)
|
|
133
|
+
throw new Error('TokenInvalidInstructionTypeError')
|
|
134
|
+
if (!mint) throw new Error('TokenInvalidInstructionKeysError')
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
programId,
|
|
138
|
+
keys: {
|
|
139
|
+
source,
|
|
140
|
+
mint,
|
|
141
|
+
destination,
|
|
142
|
+
authority,
|
|
143
|
+
signers: signers || null,
|
|
144
|
+
},
|
|
145
|
+
data,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Decode a TransferCheckedWithFees instruction without validating it
|
|
151
|
+
*
|
|
152
|
+
* @param instruction Transaction instruction to decode
|
|
153
|
+
*
|
|
154
|
+
* @return Decoded, non-validated instruction
|
|
155
|
+
*/
|
|
156
|
+
export function decodeTransferCheckedWithFeeInstructionUnchecked({
|
|
157
|
+
programId,
|
|
158
|
+
keys: [source, mint, destination, authority, ...signers],
|
|
159
|
+
data,
|
|
160
|
+
}) {
|
|
161
|
+
const { instruction, transferFeeInstruction, amount, decimals, fee } =
|
|
162
|
+
transferCheckedWithFeeInstructionData.decode(data)
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
programId,
|
|
166
|
+
keys: {
|
|
167
|
+
source,
|
|
168
|
+
mint,
|
|
169
|
+
destination,
|
|
170
|
+
authority,
|
|
171
|
+
signers,
|
|
172
|
+
},
|
|
173
|
+
data: {
|
|
174
|
+
instruction,
|
|
175
|
+
transferFeeInstruction,
|
|
176
|
+
amount,
|
|
177
|
+
decimals,
|
|
178
|
+
fee,
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// utils
|
|
184
|
+
|
|
185
|
+
export function addSigners(
|
|
186
|
+
keys, // AccountMeta[],
|
|
187
|
+
ownerOrAuthority, // PublicKey,
|
|
188
|
+
multiSigners // (Signer | PublicKey)[]
|
|
189
|
+
) {
|
|
190
|
+
if (multiSigners.length > 0) {
|
|
191
|
+
keys.push({ pubkey: ownerOrAuthority, isSigner: false, isWritable: false })
|
|
192
|
+
for (const signer of multiSigners) {
|
|
193
|
+
keys.push({
|
|
194
|
+
pubkey: signer instanceof PublicKey ? signer : signer.publicKey,
|
|
195
|
+
isSigner: true,
|
|
196
|
+
isWritable: false,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
keys.push({ pubkey: ownerOrAuthority, isSigner: true, isWritable: false })
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return keys
|
|
204
|
+
}
|
|
@@ -7,21 +7,24 @@ import { findAssociatedTokenAddress } from '../encode'
|
|
|
7
7
|
export const createAssociatedTokenAccount = (
|
|
8
8
|
senderAddress,
|
|
9
9
|
tokenMintAddress,
|
|
10
|
-
ownerAddress // destination SOL address
|
|
10
|
+
ownerAddress, // destination SOL address
|
|
11
|
+
tokenProgram
|
|
11
12
|
) => {
|
|
12
13
|
const associatedTokenAccountPublicKey = new PublicKey(
|
|
13
|
-
findAssociatedTokenAddress(ownerAddress, tokenMintAddress)
|
|
14
|
+
findAssociatedTokenAddress(ownerAddress, tokenMintAddress, tokenProgram)
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
const feePayerPublicKey = new PublicKey(senderAddress)
|
|
17
18
|
const ownerPublicKey = new PublicKey(ownerAddress)
|
|
18
19
|
const tokenMintPublicKey = new PublicKey(tokenMintAddress)
|
|
20
|
+
const tokenProgramPublicKey = new PublicKey(tokenProgram)
|
|
19
21
|
|
|
20
22
|
return createIx(
|
|
21
23
|
feePayerPublicKey, // feePayer
|
|
22
24
|
associatedTokenAccountPublicKey,
|
|
23
25
|
ownerPublicKey,
|
|
24
|
-
tokenMintPublicKey
|
|
26
|
+
tokenMintPublicKey,
|
|
27
|
+
tokenProgramPublicKey
|
|
25
28
|
) // returns the instruction
|
|
26
29
|
}
|
|
27
30
|
|
|
@@ -29,7 +32,8 @@ function createIx(
|
|
|
29
32
|
funderPubkey,
|
|
30
33
|
associatedTokenAccountPublicKey,
|
|
31
34
|
ownerPublicKey,
|
|
32
|
-
tokenMintPublicKey
|
|
35
|
+
tokenMintPublicKey,
|
|
36
|
+
tokenProgramPublicKey
|
|
33
37
|
) {
|
|
34
38
|
return new TransactionInstruction({
|
|
35
39
|
programId: SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
|
|
@@ -44,7 +48,7 @@ function createIx(
|
|
|
44
48
|
{ pubkey: ownerPublicKey, isSigner: false, isWritable: false },
|
|
45
49
|
{ pubkey: tokenMintPublicKey, isSigner: false, isWritable: false },
|
|
46
50
|
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
47
|
-
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
51
|
+
{ pubkey: tokenProgramPublicKey || TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
48
52
|
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
|
|
49
53
|
],
|
|
50
54
|
})
|
package/src/transaction.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
createTokenTransferInstruction,
|
|
11
11
|
createCloseAccountInstruction,
|
|
12
12
|
} from './helpers/tokenTransfer'
|
|
13
|
+
import { createTransferCheckedWithFeeInstruction } from './helpers/spl-token-2022'
|
|
13
14
|
import {
|
|
14
15
|
PublicKey,
|
|
15
16
|
Account,
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
TransactionInstruction,
|
|
23
24
|
} from './vendor'
|
|
24
25
|
import { MagicEdenEscrowProgram } from './magiceden/escrow-program'
|
|
25
|
-
import { MEMO_PROGRAM_ID, SEED, STAKE_PROGRAM_ID } from './constants'
|
|
26
|
+
import { MEMO_PROGRAM_ID, SEED, STAKE_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from './constants'
|
|
26
27
|
|
|
27
28
|
class Tx {
|
|
28
29
|
constructor({
|
|
@@ -60,6 +61,7 @@ class Tx {
|
|
|
60
61
|
'isAssociatedTokenAccountActive is required when sending tokens'
|
|
61
62
|
) // needed to create the recipient account
|
|
62
63
|
assert(Array.isArray(fromTokenAddresses), 'fromTokenAddresses Array is required')
|
|
64
|
+
assert(fromTokenAddresses.length > 0, 'fromTokenAddresses is empty')
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
this.txObj = {
|
|
@@ -157,13 +159,23 @@ class Tx {
|
|
|
157
159
|
// const isUnknown = destinationAddressType === null
|
|
158
160
|
// if (isUnknown) throw new Error('Destination SOL balance cannot be zero (address not active)') // cannot initialize without knowing the owner
|
|
159
161
|
const isSOLaddress = ['solana', null].includes(destinationAddressType)
|
|
162
|
+
const rawTokenProgram = fromTokenAddresses[0]?.tokenProgram
|
|
163
|
+
if (!rawTokenProgram) throw new Error('Cannot detect token program')
|
|
164
|
+
const tokenProgram = new PublicKey(rawTokenProgram).toBase58()
|
|
160
165
|
// crete account instruction
|
|
161
166
|
if (isSOLaddress && !isAssociatedTokenAccountActive)
|
|
162
|
-
this.transaction.add(createAssociatedTokenAccount(from, tokenMintAddress, to))
|
|
167
|
+
this.transaction.add(createAssociatedTokenAccount(from, tokenMintAddress, to, tokenProgram))
|
|
163
168
|
|
|
164
169
|
let amountLeft = amount
|
|
165
170
|
let amountToSend
|
|
166
|
-
for (let {
|
|
171
|
+
for (let {
|
|
172
|
+
mintAddress,
|
|
173
|
+
tokenAccountAddress,
|
|
174
|
+
balance,
|
|
175
|
+
decimals,
|
|
176
|
+
feeBasisPoints,
|
|
177
|
+
maximumFee,
|
|
178
|
+
} of fromTokenAddresses) {
|
|
167
179
|
// need to add more of this instruction until we reach the desired balance (amount) to send
|
|
168
180
|
assert(mintAddress === tokenMintAddress, `Got unexpected mintAddress ${mintAddress}`)
|
|
169
181
|
if (amountLeft === 0) break
|
|
@@ -177,13 +189,32 @@ class Tx {
|
|
|
177
189
|
amountLeft -= amountToSend
|
|
178
190
|
}
|
|
179
191
|
|
|
180
|
-
const dest = isSOLaddress
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
192
|
+
const dest = isSOLaddress
|
|
193
|
+
? findAssociatedTokenAddress(to, tokenMintAddress, tokenProgram)
|
|
194
|
+
: to
|
|
195
|
+
let tokenTransferInstruction
|
|
196
|
+
if (tokenProgram === TOKEN_2022_PROGRAM_ID.toBase58()) {
|
|
197
|
+
// token transfer fee
|
|
198
|
+
const fee = Math.ceil((amountToSend * feeBasisPoints) / 10_000)
|
|
199
|
+
const feeCharged = fee > maximumFee ? maximumFee : fee
|
|
200
|
+
|
|
201
|
+
tokenTransferInstruction = createTransferCheckedWithFeeInstruction(
|
|
202
|
+
tokenAccountAddress,
|
|
203
|
+
tokenMintAddress,
|
|
204
|
+
dest,
|
|
205
|
+
from,
|
|
206
|
+
amountToSend,
|
|
207
|
+
decimals, // token decimals
|
|
208
|
+
feeCharged // token fee (not SOL fee)
|
|
209
|
+
)
|
|
210
|
+
} else {
|
|
211
|
+
tokenTransferInstruction = createTokenTransferInstruction(
|
|
212
|
+
from,
|
|
213
|
+
tokenAccountAddress,
|
|
214
|
+
dest,
|
|
215
|
+
amountToSend
|
|
216
|
+
)
|
|
217
|
+
}
|
|
187
218
|
|
|
188
219
|
// If reference accounts are provided, add them to the transfer instruction
|
|
189
220
|
if (reference) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { PublicKey } from '../vendor/publickey'
|
|
2
|
+
|
|
3
|
+
export class AsyncSignerAccount {
|
|
4
|
+
#publicKey
|
|
5
|
+
#asyncSigner
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a new AsyncSignerAccount object
|
|
9
|
+
*
|
|
10
|
+
* @param asyncSigner A signer object: `{ sign, getPublicKey }`
|
|
11
|
+
*/
|
|
12
|
+
constructor(asyncSigner) {
|
|
13
|
+
if (!asyncSigner) throw new Error('please provide a signer object to async signer')
|
|
14
|
+
|
|
15
|
+
this.#asyncSigner = asyncSigner
|
|
16
|
+
this.updatePublicKey().catch((err) =>
|
|
17
|
+
console.error('error getting public key from signer', err)
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get publicKey() {
|
|
22
|
+
if (!this.#publicKey) throw new Error('public key not yet available in async signer')
|
|
23
|
+
return this.#publicKey
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get secretKey() {
|
|
27
|
+
throw new Error('secret key not available from async signer')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
sign = async (signData) => this.#asyncSigner.sign({ data: signData })
|
|
31
|
+
|
|
32
|
+
updatePublicKey = async () => {
|
|
33
|
+
if (!this.#publicKey) {
|
|
34
|
+
const publicKey = await this.#asyncSigner.getPublicKey()
|
|
35
|
+
this.#publicKey = new PublicKey(publicKey)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -12,6 +12,7 @@ export function createUnsignedTx({
|
|
|
12
12
|
isAssociatedTokenAccountActive, // true when recipient balance !== 0
|
|
13
13
|
fromTokenAddresses, // sender token addresses
|
|
14
14
|
tokenStandard,
|
|
15
|
+
tokenProgram,
|
|
15
16
|
// Program interactions:
|
|
16
17
|
method,
|
|
17
18
|
// Staking related:
|
|
@@ -48,6 +49,7 @@ export function createUnsignedTx({
|
|
|
48
49
|
isAssociatedTokenAccountActive,
|
|
49
50
|
fromTokenAddresses,
|
|
50
51
|
tokenStandard,
|
|
52
|
+
tokenProgram,
|
|
51
53
|
// Staking related:
|
|
52
54
|
method,
|
|
53
55
|
stakeAddresses,
|
|
@@ -3,44 +3,63 @@ import assert from 'minimalistic-assert'
|
|
|
3
3
|
import { prepareForSigning } from './prepare-for-signing'
|
|
4
4
|
import { getKeyPairFromPrivateKey } from '../keypair'
|
|
5
5
|
import { Account } from '../vendor'
|
|
6
|
+
import { AsyncSignerAccount } from './async-account'
|
|
6
7
|
import { extractTransaction, isVersionedTransaction } from './common'
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
*
|
|
11
|
+
* @param {*} unsignedTx
|
|
12
|
+
* @param {Object} asyncSigner object: `{ sign: async (buffer) => Promise<>, getPublicKey: async () => Promise<any>}`
|
|
13
|
+
* @returns
|
|
14
|
+
*/
|
|
15
|
+
export async function signUnsignedTxWithSigner(unsignedTx, signer) {
|
|
16
|
+
assert(signer, 'Please provide a signer')
|
|
17
|
+
|
|
18
|
+
const tx = prepareForSigning(unsignedTx)
|
|
19
|
+
|
|
20
|
+
const account = new AsyncSignerAccount(signer)
|
|
21
|
+
await account.updatePublicKey()
|
|
22
|
+
await _signTx({ tx, account })
|
|
23
|
+
|
|
24
|
+
return extractTransaction({ tx })
|
|
25
|
+
}
|
|
26
|
+
|
|
8
27
|
export function signUnsignedTx(unsignedTx, privateKey) {
|
|
9
28
|
assert(privateKey, 'Please provide a secretKey')
|
|
10
29
|
|
|
11
30
|
const tx = prepareForSigning(unsignedTx)
|
|
12
31
|
|
|
13
|
-
|
|
32
|
+
const { secretKey } = getKeyPairFromPrivateKey(privateKey)
|
|
33
|
+
const account = new Account(secretKey)
|
|
34
|
+
_signTx({ tx, account })
|
|
14
35
|
|
|
15
36
|
return extractTransaction({ tx })
|
|
16
37
|
}
|
|
17
38
|
|
|
18
39
|
// Signs plain tx.
|
|
19
|
-
const _signTx = ({ tx,
|
|
20
|
-
const { secretKey } = getKeyPairFromPrivateKey(privateKey)
|
|
21
|
-
const account = new Account(secretKey)
|
|
40
|
+
const _signTx = ({ tx, account }) => {
|
|
22
41
|
if (isVersionedTransaction(tx)) {
|
|
23
42
|
// VersionedTransaction
|
|
24
|
-
tx.sign([account])
|
|
25
|
-
} else {
|
|
26
|
-
// Legacy Transactions
|
|
27
|
-
|
|
28
|
-
// Some transactions that we construct internally are technically not complete.
|
|
29
|
-
// They don't contain the empty signature slot for the public key.
|
|
30
|
-
const foundEmptySignatureSlot = tx.signatures.find(({ publicKey }) =>
|
|
31
|
-
publicKey.equals(account.publicKey)
|
|
32
|
-
)
|
|
33
|
-
if (!foundEmptySignatureSlot) {
|
|
34
|
-
// We could use `setSigners` but maybe this is more robust?
|
|
35
|
-
tx.signatures.push({
|
|
36
|
-
publicKey: account.publicKey,
|
|
37
|
-
signature: null,
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// We need to use `partialSign()` here because legacy `sign()` will
|
|
42
|
-
// delete all existing signatures which isn't great if we're
|
|
43
|
-
// signing a transaction that already has signatures.
|
|
44
|
-
tx.partialSign(account)
|
|
43
|
+
return tx.sign([account])
|
|
45
44
|
}
|
|
45
|
+
|
|
46
|
+
// Legacy Transactions
|
|
47
|
+
|
|
48
|
+
// Some transactions that we construct internally are technically not complete.
|
|
49
|
+
// They don't contain the empty signature slot for the public key.
|
|
50
|
+
const foundEmptySignatureSlot = tx.signatures.find(({ publicKey }) =>
|
|
51
|
+
publicKey.equals(account.publicKey)
|
|
52
|
+
)
|
|
53
|
+
if (!foundEmptySignatureSlot) {
|
|
54
|
+
// We could use `setSigners` but maybe this is more robust?
|
|
55
|
+
tx.signatures.push({
|
|
56
|
+
publicKey: account.publicKey,
|
|
57
|
+
signature: null,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// We need to use `partialSign()` here because legacy `sign()` will
|
|
62
|
+
// delete all existing signatures which isn't great if we're
|
|
63
|
+
// signing a transaction that already has signatures.
|
|
64
|
+
return tx.partialSign(account)
|
|
46
65
|
}
|
|
@@ -21,6 +21,8 @@ import * as shortvec from './utils/shortvec-encoding'
|
|
|
21
21
|
const DEFAULT_SIGNATURE = Buffer.alloc(64).fill(0)
|
|
22
22
|
const SIGNATURE_LENGTH = 64
|
|
23
23
|
|
|
24
|
+
const isAsyncAccount = (signer) => signer.sign && signer.publicKey && signer.updatePublicKey
|
|
25
|
+
|
|
24
26
|
/**
|
|
25
27
|
* Account metadata used to define instructions
|
|
26
28
|
*
|
|
@@ -412,7 +414,7 @@ export class Transaction {
|
|
|
412
414
|
publicKey: signer.publicKey,
|
|
413
415
|
}))
|
|
414
416
|
|
|
415
|
-
this.partialSign(...signers)
|
|
417
|
+
return this.partialSign(...signers)
|
|
416
418
|
}
|
|
417
419
|
|
|
418
420
|
/**
|
|
@@ -434,6 +436,21 @@ export class Transaction {
|
|
|
434
436
|
})
|
|
435
437
|
|
|
436
438
|
const signData = message.serialize()
|
|
439
|
+
|
|
440
|
+
const isAsyncSign = signers.some(isAsyncAccount)
|
|
441
|
+
if (isAsyncSign) {
|
|
442
|
+
return Promise.all(
|
|
443
|
+
signers.map(async (signer) => ({
|
|
444
|
+
signature: isAsyncAccount(signer)
|
|
445
|
+
? await signer.sign(signData) // Promise
|
|
446
|
+
: nacl.sign.detached(signData, signer.secretKey),
|
|
447
|
+
publicKey: signer.publicKey,
|
|
448
|
+
}))
|
|
449
|
+
).then((signatures) =>
|
|
450
|
+
signatures.forEach(({ publicKey, signature }) => this.addSignature(publicKey, signature))
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
|
|
437
454
|
signers.forEach((signer) => {
|
|
438
455
|
const signature = nacl.sign.detached(signData, signer.secretKey)
|
|
439
456
|
this.addSignature(signer.publicKey, signature)
|