@exodus/solana-lib 1.2.9-build2 → 1.2.10
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 +5 -4
- package/src/constants.js +12 -0
- package/src/encode.js +57 -0
- package/src/fee-data/index.js +1 -0
- package/src/fee-data/solana.js +9 -0
- package/src/helpers/spl-token.js +108 -0
- package/src/helpers/tokenTransfer.js +72 -0
- package/src/index.js +9 -0
- package/src/keypair.js +32 -0
- package/src/tokens.js +19 -0
- package/src/transaction.js +292 -0
- package/src/tx/create-and-sign-tx.js +8 -0
- package/{lib → src}/tx/create-unsigned-tx.js +11 -18
- package/src/tx/index.js +4 -0
- package/src/tx/parse-unsigned-tx.js +41 -0
- package/src/tx/sign-unsigned-tx.js +78 -0
- package/src/vendor/account.js +38 -0
- package/src/vendor/index.js +9 -0
- package/src/vendor/instruction.js +46 -0
- package/src/vendor/message.js +216 -0
- package/src/vendor/nonce-account.js +46 -0
- package/src/vendor/publickey.js +212 -0
- package/src/vendor/stake-program.js +527 -0
- package/src/vendor/system-program.js +782 -0
- package/src/vendor/sysvar.js +16 -0
- package/src/vendor/transaction.js +594 -0
- package/src/vendor/utils/blockhash.js +6 -0
- package/src/vendor/utils/fee-calculator.js +17 -0
- package/src/vendor/utils/layout.js +80 -0
- package/src/vendor/utils/shortvec-encoding.js +30 -0
- package/src/vendor/utils/to-buffer.js +9 -0
- package/lib/constants.js +0 -19
- package/lib/encode.js +0 -67
- package/lib/fee-data/index.js +0 -15
- package/lib/fee-data/solana.js +0 -14
- package/lib/helpers/spl-token.js +0 -122
- package/lib/helpers/tokenTransfer.js +0 -78
- package/lib/index.js +0 -88
- package/lib/keypair.js +0 -38
- package/lib/tokens.js +0 -21
- package/lib/transaction.js +0 -338
- package/lib/tx/create-and-sign-tx.js +0 -15
- package/lib/tx/index.js +0 -53
- package/lib/tx/parse-unsigned-tx.js +0 -55
- package/lib/tx/sign-unsigned-tx.js +0 -90
- package/lib/vendor/account.js +0 -48
- package/lib/vendor/index.js +0 -113
- package/lib/vendor/instruction.js +0 -48
- package/lib/vendor/message.js +0 -167
- package/lib/vendor/nonce-account.js +0 -56
- package/lib/vendor/publickey.js +0 -211
- package/lib/vendor/stake-program.js +0 -476
- package/lib/vendor/system-program.js +0 -640
- package/lib/vendor/sysvar.js +0 -19
- package/lib/vendor/transaction.js +0 -594
- package/lib/vendor/utils/blockhash.js +0 -1
- package/lib/vendor/utils/fee-calculator.js +0 -25
- package/lib/vendor/utils/layout.js +0 -97
- package/lib/vendor/utils/shortvec-encoding.js +0 -41
- package/lib/vendor/utils/to-buffer.js +0 -18
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-lib",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.10",
|
|
4
4
|
"description": "Exodus internal Solana low-level library",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
7
|
-
"
|
|
7
|
+
"src/",
|
|
8
8
|
"!src/__tests__"
|
|
9
9
|
],
|
|
10
10
|
"author": "Exodus",
|
|
@@ -21,5 +21,6 @@
|
|
|
21
21
|
"bs58": "~4.0.1",
|
|
22
22
|
"create-hash": "~1.1.3",
|
|
23
23
|
"tweetnacl": "^1.0.3"
|
|
24
|
-
}
|
|
24
|
+
},
|
|
25
|
+
"gitHead": "9ea01d6b464b1a6a23e49f3634b9e661e755efc8"
|
|
25
26
|
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PublicKey, SystemProgram, StakeProgram } from './vendor'
|
|
2
|
+
|
|
3
|
+
export const SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = new PublicKey(
|
|
4
|
+
'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'
|
|
5
|
+
)
|
|
6
|
+
export const SYSTEM_PROGRAM_ID = SystemProgram.programId
|
|
7
|
+
|
|
8
|
+
export const STAKE_PROGRAM_ID = StakeProgram.programId
|
|
9
|
+
|
|
10
|
+
export const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA')
|
|
11
|
+
|
|
12
|
+
export const SEED = 'stake:0'
|
package/src/encode.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import { PublicKey, StakeProgram } from './vendor'
|
|
3
|
+
import { SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, TOKEN_PROGRAM_ID, SEED } from './constants'
|
|
4
|
+
import { getPublicKey } from './keypair'
|
|
5
|
+
import bs58 from 'bs58'
|
|
6
|
+
import BN from 'bn.js'
|
|
7
|
+
|
|
8
|
+
export function getAddressFromPublicKey(publicKey: string | Buffer): string {
|
|
9
|
+
return bs58.encode(Buffer.from(publicKey, 'hex'))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getAddressFromPrivateKey(privateKey: string | Buffer): string {
|
|
13
|
+
return getAddressFromPublicKey(getPublicKey(privateKey))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isValidAddress(address: string): boolean {
|
|
17
|
+
try {
|
|
18
|
+
// assume base 58 encoding by default
|
|
19
|
+
const decoded = bs58.decode(address)
|
|
20
|
+
if (decoded.length !== 32) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
const _bn = new BN(decoded)
|
|
24
|
+
if (_bn.byteLength() > 32) {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
return true
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// doc: https://spl.solana.com/associated-token-account (HACK: refactored to sync)
|
|
34
|
+
export function findAssociatedTokenAddress(
|
|
35
|
+
walletAddress: string,
|
|
36
|
+
tokenMintAddress: string
|
|
37
|
+
): string {
|
|
38
|
+
walletAddress = new PublicKey(walletAddress)
|
|
39
|
+
tokenMintAddress = new PublicKey(tokenMintAddress)
|
|
40
|
+
return PublicKey.findProgramAddress(
|
|
41
|
+
[walletAddress.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), tokenMintAddress.toBuffer()],
|
|
42
|
+
SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID
|
|
43
|
+
)[0].toBase58() // returns encoded PublicKey
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createStakeAddress(walletAddress: string, seed = SEED): string {
|
|
47
|
+
const fromPubkey = new PublicKey(walletAddress)
|
|
48
|
+
|
|
49
|
+
const newAccountPubkey = PublicKey.createWithSeed(
|
|
50
|
+
// HACK: refactored to sync
|
|
51
|
+
fromPubkey,
|
|
52
|
+
seed,
|
|
53
|
+
StakeProgram.programId
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return newAccountPubkey.toBase58()
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as solana } from './solana'
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import BN from 'bn.js'
|
|
3
|
+
import * as BufferLayout from '@exodus/buffer-layout'
|
|
4
|
+
import { Account, PublicKey, TransactionInstruction } from '../vendor'
|
|
5
|
+
import * as Layout from '../vendor/utils/layout'
|
|
6
|
+
|
|
7
|
+
// Extracted from https://github.com/ExodusMovement/solana-spl-token/blob/master/src/index.js#L263
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 64-bit value
|
|
11
|
+
*/
|
|
12
|
+
export class U64 extends BN {
|
|
13
|
+
/**
|
|
14
|
+
* Convert to Buffer representation
|
|
15
|
+
*/
|
|
16
|
+
toBuffer(): typeof Buffer {
|
|
17
|
+
const a = super.toArray().reverse()
|
|
18
|
+
const b = Buffer.from(a)
|
|
19
|
+
if (b.length === 8) {
|
|
20
|
+
return b
|
|
21
|
+
}
|
|
22
|
+
assert(b.length < 8, 'u64 too large')
|
|
23
|
+
|
|
24
|
+
const zeroPad = Buffer.alloc(8)
|
|
25
|
+
b.copy(zeroPad)
|
|
26
|
+
return zeroPad
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Construct a u64 from Buffer representation
|
|
31
|
+
*/
|
|
32
|
+
static fromBuffer(buffer: typeof Buffer): U64 {
|
|
33
|
+
assert(buffer.length === 8, `Invalid buffer length: ${buffer.length}`)
|
|
34
|
+
return new U64(
|
|
35
|
+
[...buffer]
|
|
36
|
+
.reverse()
|
|
37
|
+
.map((i) => `00${i.toString(16)}`.slice(-2))
|
|
38
|
+
.join(''),
|
|
39
|
+
16
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const u64 = U64 // alias
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* An ERC20-like Token
|
|
48
|
+
*/
|
|
49
|
+
export class Token {
|
|
50
|
+
/**
|
|
51
|
+
* Construct a Transfer instruction
|
|
52
|
+
*
|
|
53
|
+
* @param programId SPL Token program account
|
|
54
|
+
* @param source Source account
|
|
55
|
+
* @param destination Destination account
|
|
56
|
+
* @param owner Owner of the source account
|
|
57
|
+
* @param multiSigners Signing accounts if `authority` is a multiSig
|
|
58
|
+
* @param amount Number of tokens to transfer
|
|
59
|
+
*/
|
|
60
|
+
static createTransferInstruction(
|
|
61
|
+
programId: PublicKey,
|
|
62
|
+
source: PublicKey,
|
|
63
|
+
destination: PublicKey,
|
|
64
|
+
owner: PublicKey,
|
|
65
|
+
multiSigners: Array<Account>,
|
|
66
|
+
amount: number | u64
|
|
67
|
+
): TransactionInstruction {
|
|
68
|
+
const dataLayout = BufferLayout.struct([
|
|
69
|
+
BufferLayout.u8('instruction'),
|
|
70
|
+
Layout.uint64('amount'),
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
const data = Buffer.alloc(dataLayout.span)
|
|
74
|
+
dataLayout.encode(
|
|
75
|
+
{
|
|
76
|
+
instruction: 3, // Transfer instruction
|
|
77
|
+
amount: new U64(amount).toBuffer(),
|
|
78
|
+
},
|
|
79
|
+
data
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
let keys = [
|
|
83
|
+
{ pubkey: source, isSigner: false, isWritable: true },
|
|
84
|
+
{ pubkey: destination, isSigner: false, isWritable: true },
|
|
85
|
+
]
|
|
86
|
+
if (multiSigners.length === 0) {
|
|
87
|
+
keys.push({
|
|
88
|
+
pubkey: owner,
|
|
89
|
+
isSigner: true,
|
|
90
|
+
isWritable: false,
|
|
91
|
+
})
|
|
92
|
+
} else {
|
|
93
|
+
keys.push({ pubkey: owner, isSigner: false, isWritable: false })
|
|
94
|
+
multiSigners.forEach((signer) =>
|
|
95
|
+
keys.push({
|
|
96
|
+
pubkey: signer.publicKey,
|
|
97
|
+
isSigner: true,
|
|
98
|
+
isWritable: false,
|
|
99
|
+
})
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
return new TransactionInstruction({
|
|
103
|
+
keys,
|
|
104
|
+
programId: programId,
|
|
105
|
+
data,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Token } from './spl-token'
|
|
2
|
+
import { PublicKey, TransactionInstruction, SystemProgram, SYSVAR_RENT_PUBKEY } from '../vendor'
|
|
3
|
+
import { SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants'
|
|
4
|
+
import { findAssociatedTokenAddress } from '../encode'
|
|
5
|
+
|
|
6
|
+
// https://github.com/paul-schaaf/spl-token-ui/blob/main/src/solana/token/associatedToken.ts#L59
|
|
7
|
+
export const createAssociatedTokenAccount = (
|
|
8
|
+
senderAddress: string,
|
|
9
|
+
tokenMintAddress: string,
|
|
10
|
+
ownerAddress: string // destination SOL address
|
|
11
|
+
) => {
|
|
12
|
+
const associatedTokenAccountPublicKey = new PublicKey(
|
|
13
|
+
findAssociatedTokenAddress(ownerAddress, tokenMintAddress)
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const feePayerPublicKey = new PublicKey(senderAddress)
|
|
17
|
+
const ownerPublicKey = new PublicKey(ownerAddress)
|
|
18
|
+
const tokenMintPublicKey = new PublicKey(tokenMintAddress)
|
|
19
|
+
|
|
20
|
+
const ix = createIx(
|
|
21
|
+
feePayerPublicKey, // feePayer
|
|
22
|
+
associatedTokenAccountPublicKey,
|
|
23
|
+
ownerPublicKey,
|
|
24
|
+
tokenMintPublicKey
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
return ix // returns the instruction
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createIx(
|
|
31
|
+
funderPubkey: PublicKey,
|
|
32
|
+
associatedTokenAccountPublicKey: PublicKey,
|
|
33
|
+
ownerPublicKey: PublicKey,
|
|
34
|
+
tokenMintPublicKey: PublicKey
|
|
35
|
+
) {
|
|
36
|
+
return new TransactionInstruction({
|
|
37
|
+
programId: SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
|
|
38
|
+
data: Buffer.from([]),
|
|
39
|
+
keys: [
|
|
40
|
+
{ pubkey: funderPubkey, isSigner: true, isWritable: true },
|
|
41
|
+
{
|
|
42
|
+
pubkey: associatedTokenAccountPublicKey,
|
|
43
|
+
isSigner: false,
|
|
44
|
+
isWritable: true,
|
|
45
|
+
},
|
|
46
|
+
{ pubkey: ownerPublicKey, isSigner: false, isWritable: false },
|
|
47
|
+
{ pubkey: tokenMintPublicKey, isSigner: false, isWritable: false },
|
|
48
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
49
|
+
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
50
|
+
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
|
|
51
|
+
],
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// https://github.com/paul-schaaf/spl-token-ui/blob/main/src/solana/token/editing.ts#L211
|
|
56
|
+
export const createTokenTransferInstruction = (owner, fromTokenAddress, to, amount) => {
|
|
57
|
+
const sourcePubkey = new PublicKey(fromTokenAddress) // the token ADDRESS needed!
|
|
58
|
+
const destinationPubkey = new PublicKey(to)
|
|
59
|
+
console.log(`destination token address: ${destinationPubkey.toBase58()}`)
|
|
60
|
+
const ownerAccountOrWalletPublicKey = new PublicKey(owner) // the only native SOL address
|
|
61
|
+
|
|
62
|
+
const transferIx = Token.createTransferInstruction(
|
|
63
|
+
TOKEN_PROGRAM_ID,
|
|
64
|
+
sourcePubkey,
|
|
65
|
+
destinationPubkey,
|
|
66
|
+
ownerAccountOrWalletPublicKey,
|
|
67
|
+
[],
|
|
68
|
+
amount
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return transferIx
|
|
72
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
export * from './constants'
|
|
4
|
+
export * from './encode'
|
|
5
|
+
export * from './keypair'
|
|
6
|
+
export * from './tx'
|
|
7
|
+
export { StakeInstruction } from './vendor'
|
|
8
|
+
export { default as tokens } from './tokens'
|
|
9
|
+
export { default as Transaction } from './transaction'
|
package/src/keypair.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import nacl from 'tweetnacl'
|
|
3
|
+
|
|
4
|
+
export function getKeyPairFromPrivateKey(privateKey: string | Buffer): Object {
|
|
5
|
+
const { publicKey, secretKey } = nacl.sign.keyPair.fromSeed(
|
|
6
|
+
Buffer.from(privateKey, 'hex').slice(0, 32)
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
secretKey: Buffer.from(secretKey, 'hex'), // secretKey
|
|
11
|
+
publicKey: Buffer.from(publicKey, 'hex'),
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getPublicKey(privateKey: string | Buffer): Buffer {
|
|
16
|
+
return getKeyPairFromPrivateKey(privateKey).publicKey
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function sign(data: Buffer, privateKey: string | Buffer): Buffer {
|
|
20
|
+
return Buffer.from(
|
|
21
|
+
nacl.sign.detached(data, getKeyPairFromPrivateKey(Buffer.from(privateKey, 'hex')).secretKey),
|
|
22
|
+
'hex'
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function verifySignature(
|
|
27
|
+
data: Buffer,
|
|
28
|
+
signature: Buffer,
|
|
29
|
+
publicKey: string | Buffer
|
|
30
|
+
): boolean {
|
|
31
|
+
return nacl.sign.detached.verify(data, signature, Buffer.from(publicKey, 'hex'))
|
|
32
|
+
}
|
package/src/tokens.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// adapted from https://github.com/project-serum/spl-token-wallet/blob/master/src/utils/tokens/names.js
|
|
2
|
+
|
|
3
|
+
const TOKENS = [
|
|
4
|
+
{
|
|
5
|
+
mintAddress: 'SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt',
|
|
6
|
+
tokenName: 'serum',
|
|
7
|
+
tokenProperName: 'Serum',
|
|
8
|
+
tokenSymbol: 'SRM',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
mintAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
|
12
|
+
tokenName: 'usdcoin_solana',
|
|
13
|
+
tokenProperName: 'USD Coin Solana',
|
|
14
|
+
tokenSymbol: 'USDCSOL',
|
|
15
|
+
},
|
|
16
|
+
// ....
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
export default TOKENS
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import assert from 'assert'
|
|
3
|
+
import bs58 from 'bs58'
|
|
4
|
+
import { get } from 'lodash'
|
|
5
|
+
|
|
6
|
+
import { getKeyPairFromPrivateKey } from './keypair'
|
|
7
|
+
import { findAssociatedTokenAddress, createStakeAddress } from './encode'
|
|
8
|
+
import {
|
|
9
|
+
createAssociatedTokenAccount,
|
|
10
|
+
createTokenTransferInstruction,
|
|
11
|
+
} from './helpers/tokenTransfer'
|
|
12
|
+
import {
|
|
13
|
+
PublicKey,
|
|
14
|
+
Account,
|
|
15
|
+
Transaction,
|
|
16
|
+
SystemProgram,
|
|
17
|
+
StakeProgram,
|
|
18
|
+
StakeInstruction,
|
|
19
|
+
Authorized,
|
|
20
|
+
Lockup,
|
|
21
|
+
} from './vendor'
|
|
22
|
+
import { SEED, STAKE_PROGRAM_ID } from './constants'
|
|
23
|
+
import TOKENS from './tokens'
|
|
24
|
+
|
|
25
|
+
class Tx {
|
|
26
|
+
constructor({
|
|
27
|
+
from,
|
|
28
|
+
to,
|
|
29
|
+
amount,
|
|
30
|
+
recentBlockhash,
|
|
31
|
+
// fee, // (Fee per Signature: 5000 lamports)
|
|
32
|
+
// Tokens related:
|
|
33
|
+
tokenName,
|
|
34
|
+
destinationAddressType,
|
|
35
|
+
isAssociatedTokenAccountActive, // true when recipient balance !== 0
|
|
36
|
+
fromTokenAddresses, // sender token addresses
|
|
37
|
+
} = {}) {
|
|
38
|
+
assert(from, 'from is required')
|
|
39
|
+
assert(to, 'to is required')
|
|
40
|
+
assert(amount, 'amount is required')
|
|
41
|
+
assert(typeof amount === 'number', 'amount must be a number')
|
|
42
|
+
assert(recentBlockhash, 'recentBlockhash is required')
|
|
43
|
+
if (tokenName) {
|
|
44
|
+
assert(
|
|
45
|
+
typeof destinationAddressType !== 'undefined',
|
|
46
|
+
'destinationAddressType is required when sending tokens'
|
|
47
|
+
)
|
|
48
|
+
assert(
|
|
49
|
+
typeof isAssociatedTokenAccountActive !== 'undefined',
|
|
50
|
+
'isAssociatedTokenAccountActive is required when sending tokens'
|
|
51
|
+
) // needed to create the recipient account
|
|
52
|
+
assert(Array.isArray(fromTokenAddresses), 'fromTokenAddresses Array is required')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const token = TOKENS.find(({ tokenName: name }) => name === tokenName)
|
|
56
|
+
this.txObj = {
|
|
57
|
+
from,
|
|
58
|
+
to,
|
|
59
|
+
amount,
|
|
60
|
+
recentBlockhash,
|
|
61
|
+
token,
|
|
62
|
+
destinationAddressType,
|
|
63
|
+
isAssociatedTokenAccountActive,
|
|
64
|
+
fromTokenAddresses,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (token) {
|
|
68
|
+
// TOKEN transfer tx
|
|
69
|
+
this.buildTokenTransaction(this.txObj)
|
|
70
|
+
} else {
|
|
71
|
+
// SOL tx
|
|
72
|
+
this.buildSOLtransaction(this.txObj)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
buildSOLtransaction({ from, to, amount, recentBlockhash }) {
|
|
77
|
+
const txInstruction = SystemProgram.transfer({
|
|
78
|
+
fromPubkey: new PublicKey(from),
|
|
79
|
+
toPubkey: new PublicKey(to),
|
|
80
|
+
lamports: amount,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
this.transaction = new Transaction({
|
|
84
|
+
instructions: [txInstruction],
|
|
85
|
+
recentBlockhash,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
buildTokenTransaction({
|
|
90
|
+
from,
|
|
91
|
+
to,
|
|
92
|
+
amount,
|
|
93
|
+
recentBlockhash,
|
|
94
|
+
token,
|
|
95
|
+
destinationAddressType,
|
|
96
|
+
isAssociatedTokenAccountActive,
|
|
97
|
+
fromTokenAddresses,
|
|
98
|
+
}) {
|
|
99
|
+
this.transaction = new Transaction({
|
|
100
|
+
recentBlockhash,
|
|
101
|
+
})
|
|
102
|
+
const isUnknown = destinationAddressType === null
|
|
103
|
+
const isSOLaddress = destinationAddressType === 'solana'
|
|
104
|
+
// crete account instruction
|
|
105
|
+
if (isUnknown) throw new Error('Destination SOL balance cannot be zero (address not active)') // cannot initialize without knowing the owner
|
|
106
|
+
if (isSOLaddress && !isAssociatedTokenAccountActive)
|
|
107
|
+
this.transaction.add(createAssociatedTokenAccount(from, token.mintAddress, to))
|
|
108
|
+
|
|
109
|
+
let amountLeft = amount
|
|
110
|
+
let amountToSend
|
|
111
|
+
for (let { ticker, tokenAccountAddress, balance } of fromTokenAddresses) {
|
|
112
|
+
// need to add more of this instruction until we reach the desired balance (amount) to send
|
|
113
|
+
assert(ticker === token.tokenSymbol, `Got unexpected ticker ${ticker}`)
|
|
114
|
+
if (amountLeft === 0) break
|
|
115
|
+
|
|
116
|
+
balance = Number(balance)
|
|
117
|
+
if (balance >= amountLeft) {
|
|
118
|
+
amountToSend = amountLeft
|
|
119
|
+
amountLeft = 0
|
|
120
|
+
} else {
|
|
121
|
+
amountToSend = balance // not enough balance
|
|
122
|
+
amountLeft -= amountToSend
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const dest = isSOLaddress ? findAssociatedTokenAddress(to, token.mintAddress) : to
|
|
126
|
+
// add transfer token instruction
|
|
127
|
+
this.transaction.add(
|
|
128
|
+
createTokenTransferInstruction(from, tokenAccountAddress, dest, amountToSend)
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
assert(amountLeft === 0, `Not enough balance to send ${amount} ${token.tokenSymbol}`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
static createStakeAccountTransaction({ address, amount, seed = SEED, pool, recentBlockhash }) {
|
|
136
|
+
const fromPubkey = new PublicKey(address)
|
|
137
|
+
const stakeAddress = createStakeAddress(address, seed)
|
|
138
|
+
const stakePublicKey = new PublicKey(stakeAddress)
|
|
139
|
+
const poolKey = new PublicKey(pool)
|
|
140
|
+
|
|
141
|
+
const authorized = new Authorized(fromPubkey, fromPubkey) // staker, withdrawer
|
|
142
|
+
const lockup = new Lockup(0, 0, fromPubkey) // no lockup
|
|
143
|
+
|
|
144
|
+
// create account instruction
|
|
145
|
+
const programTx = StakeProgram.createAccountWithSeed({
|
|
146
|
+
fromPubkey,
|
|
147
|
+
stakePubkey: stakePublicKey,
|
|
148
|
+
basePubkey: fromPubkey,
|
|
149
|
+
seed,
|
|
150
|
+
authorized,
|
|
151
|
+
lockup,
|
|
152
|
+
lamports: amount, // number
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// delegate funds instruction
|
|
156
|
+
const delegateTx = StakeProgram.delegate({
|
|
157
|
+
stakePubkey: stakePublicKey,
|
|
158
|
+
authorizedPubkey: fromPubkey,
|
|
159
|
+
votePubkey: poolKey, // pool vote key
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const transaction = new Transaction({ recentBlockhash }).add(programTx).add(delegateTx)
|
|
163
|
+
return transaction
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
static undelegate({ address, stakeAddresses, recentBlockhash }) {
|
|
167
|
+
// undelegate all stake addresses
|
|
168
|
+
assert(Array.isArray(stakeAddresses), 'stakeAddresses Array is required')
|
|
169
|
+
|
|
170
|
+
const fromPubkey = new PublicKey(address)
|
|
171
|
+
const transaction = new Transaction({ recentBlockhash })
|
|
172
|
+
|
|
173
|
+
stakeAddresses.forEach((stakeAddress) => {
|
|
174
|
+
const stakePublicKey = new PublicKey(stakeAddress)
|
|
175
|
+
const programTx = StakeProgram.deactivate({
|
|
176
|
+
stakePubkey: stakePublicKey,
|
|
177
|
+
authorizedPubkey: fromPubkey,
|
|
178
|
+
})
|
|
179
|
+
transaction.add(programTx)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return transaction
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
static withdraw({ address, stakeAddresses, amount, recentBlockhash }) {
|
|
186
|
+
const fromPubkey = new PublicKey(address)
|
|
187
|
+
const stakeAddress = Array.isArray(stakeAddresses) ? stakeAddresses[0] : stakeAddresses
|
|
188
|
+
const stakePublicKey = new PublicKey(stakeAddress)
|
|
189
|
+
|
|
190
|
+
const transaction = StakeProgram.withdraw({
|
|
191
|
+
stakePubkey: stakePublicKey,
|
|
192
|
+
authorizedPubkey: fromPubkey,
|
|
193
|
+
toPubkey: fromPubkey,
|
|
194
|
+
lamports: amount,
|
|
195
|
+
})
|
|
196
|
+
transaction.recentBlockhash = recentBlockhash
|
|
197
|
+
return transaction
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
static sign(tx, privateKey: Buffer | string) {
|
|
201
|
+
if (!privateKey) throw new Error('Please provide a secretKey')
|
|
202
|
+
const { secretKey } = getKeyPairFromPrivateKey(privateKey)
|
|
203
|
+
const signers = [new Account(secretKey)]
|
|
204
|
+
|
|
205
|
+
let transaction = tx
|
|
206
|
+
if (tx instanceof Tx) transaction = tx.transaction
|
|
207
|
+
|
|
208
|
+
transaction.sign(...signers)
|
|
209
|
+
if (!transaction.signature) {
|
|
210
|
+
throw new Error('!signature') // should never happen
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
serialize() {
|
|
215
|
+
const wireTransaction = this.transaction.serialize()
|
|
216
|
+
return wireTransaction.toString('base64')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
static serialize(tx) {
|
|
220
|
+
let transaction = tx
|
|
221
|
+
if (tx instanceof Tx) transaction = tx.transaction
|
|
222
|
+
return transaction.serialize().toString('base64')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
static decodeStakingTx(serialized: string) {
|
|
226
|
+
// as base64
|
|
227
|
+
const wireTransaction = Buffer.from(serialized, 'base64')
|
|
228
|
+
const tx = Transaction.from(wireTransaction) // Transaction instance
|
|
229
|
+
|
|
230
|
+
const txId = bs58.encode(tx.signature)
|
|
231
|
+
|
|
232
|
+
const stakingInstructions = tx.instructions.filter(
|
|
233
|
+
(ix) => ix.programId.toString() === STAKE_PROGRAM_ID.toString()
|
|
234
|
+
)
|
|
235
|
+
const isStakingTx = !!stakingInstructions.length
|
|
236
|
+
|
|
237
|
+
if (!isStakingTx) return null // normal transfer tx
|
|
238
|
+
|
|
239
|
+
const info = {
|
|
240
|
+
txId,
|
|
241
|
+
owner: tx.feePayer.toString(), // SOL sender
|
|
242
|
+
}
|
|
243
|
+
const txDetails = stakingInstructions.reduce((info, ix) => {
|
|
244
|
+
const type = StakeInstruction.decodeInstructionType(ix)
|
|
245
|
+
switch (type) {
|
|
246
|
+
case 'Delegate':
|
|
247
|
+
info.type = 'Delegate'
|
|
248
|
+
info.stakeAddress = get(ix, 'keys[0].pubkey', '').toString()
|
|
249
|
+
info.validator = get(ix, 'keys[1].pubkey', '').toString() // pool
|
|
250
|
+
return info
|
|
251
|
+
|
|
252
|
+
case 'Deactivate': // undelegate
|
|
253
|
+
info.type = 'Deactivate'
|
|
254
|
+
info.stakeAddress = get(ix, 'keys[0].pubkey', '').toString()
|
|
255
|
+
// TODO: could have multiple addresses undelegating
|
|
256
|
+
return info
|
|
257
|
+
|
|
258
|
+
case 'Withdraw':
|
|
259
|
+
info.type = 'Withdraw'
|
|
260
|
+
info.stakeAddress = get(ix, 'keys[0].pubkey', '').toString()
|
|
261
|
+
const { lamports, toPubkey } = StakeInstruction.decodeWithdraw(ix)
|
|
262
|
+
info.to = toPubkey.toString()
|
|
263
|
+
info.lamports = lamports.toString()
|
|
264
|
+
return info
|
|
265
|
+
|
|
266
|
+
default:
|
|
267
|
+
// skip unknown instruction type
|
|
268
|
+
return info
|
|
269
|
+
}
|
|
270
|
+
}, info)
|
|
271
|
+
|
|
272
|
+
return txDetails
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
getTxId() {
|
|
276
|
+
if (!this.transaction.signature) {
|
|
277
|
+
throw new Error('Cannot get txId, tx is not signed')
|
|
278
|
+
}
|
|
279
|
+
return bs58.encode(this.transaction.signature)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
static getTxId(tx) {
|
|
283
|
+
let transaction = tx
|
|
284
|
+
if (tx instanceof Tx) transaction = tx.transaction
|
|
285
|
+
if (!transaction.signature) {
|
|
286
|
+
throw new Error('Cannot get txId, tx is not signed')
|
|
287
|
+
}
|
|
288
|
+
return bs58.encode(transaction.signature)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export default Tx
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createUnsignedTx } from './create-unsigned-tx'
|
|
2
|
+
import { signUnsignedTx } from './sign-unsigned-tx'
|
|
3
|
+
import type { ParsedTransaction, SignedTransaction } from '@exodus/models/lib/types'
|
|
4
|
+
|
|
5
|
+
export function createAndSignTx(input: ParsedTransaction, privateKey: Buffer): SignedTransaction {
|
|
6
|
+
const unsignedTx = createUnsignedTx(input)
|
|
7
|
+
return signUnsignedTx(unsignedTx, privateKey)
|
|
8
|
+
}
|