@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-lib",
3
- "version": "2.0.0",
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/solana-meta": "^1.0.7",
37
- "@solana/web3.js": "^1.90.0"
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": "1f9f097ac7c8d4c657ef1a4786af7781bcfd7660"
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(walletAddress, tokenMintAddress) {
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(), TOKEN_PROGRAM_ID.toBuffer(), tokenMintAddress.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: TOKEN_PROGRAM_ID,
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
  })
@@ -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 { mintAddress, tokenAccountAddress, balance } of fromTokenAddresses) {
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 ? findAssociatedTokenAddress(to, tokenMintAddress) : to
181
- const tokenTransferInstruction = createTokenTransferInstruction(
182
- from,
183
- tokenAccountAddress,
184
- dest,
185
- amountToSend
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
- _signTx({ tx, privateKey })
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, privateKey }) => {
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)