UNPKG

micro-sol-signer

Version:

Create, sign & decode Solana transactions with minimum deps

847 lines (801 loc) 29.1 kB
import { ed25519 } from '@noble/curves/ed25519'; import { base58, base64, hex, utf8 } from '@scure/base'; import { sha256 } from '@noble/hashes/sha256'; import * as P from 'micro-packed'; export type Bytes = Uint8Array; export const PRECISION = 9; export const Decimal = P.coders.decimal(PRECISION); // first bit -- terminator (1 -- continue, 0 -- last) export const shortVec = P.wrap({ encodeStream: (w: P.Writer, value: number) => { if (!value) return w.byte(0); for (; value; value >>= 7) { w.bits(value > 0x7f ? 1 : 0, 1); w.bits(value & 0x7f, 7); } }, decodeStream: (r: P.Reader): number => { let len = 0; for (let pos = 0; !r.isEnd(); pos++) { const last = !r.bits(1); len |= r.bits(7) << (pos * 7); if (last) break; } return len; }, }); const rustString = P.string(P.padRight(8, P.U32LE, undefined)); const b58 = () => { const inner = P.bytes(32); return P.wrap({ size: inner.size, encodeStream: (w: P.Writer, value: string) => inner.encodeStream(w, base58.decode(value)), decodeStream: (r: P.Reader): string => base58.encode(inner.decodeStream(r)), }); }; const pubKey = b58(); export const Message = P.struct({ requiredSignatures: P.U8, readSigned: P.U8, readUnsigned: P.U8, keys: P.array(shortVec, pubKey), blockhash: pubKey, instructions: P.array( shortVec, P.struct({ programIdx: P.U8, keys: P.array(shortVec, P.U8), data: P.bytes(shortVec) }) ), }); export function validateAddress(address: string) { const pubkey = base58.decode(address); if (pubkey.length !== 32) throw new Error('Invalid Solana address'); } export type Account = { address: string; sign: boolean; write: boolean }; export type Instruction = { program: string; keys: Account[]; data: Bytes }; export type Message = { // First account in list of signers pays fee, however it is easy to make mistake, so we force user to specify feePayer manually. feePayer: string; blockhash: string; instructions: Instruction[]; }; const keyParams = (i: number, req: number, signed: number, unsigned: number, total: number) => ({ sign: i < req ? true : false, write: i < req - signed || (i >= req && i < total - unsigned) ? true : false, }); export const TransactionRaw = P.struct({ signatures: P.array(shortVec, P.bytes(64)), msg: Message, }); // doesn't verify signatures, just parses them export type Tx = { msg: Message; signatures: Record<string, Bytes> }; // Keys position is implementation specific, Transaction.encode(Transaction.decode(tx)) not neccessary equals to tx, // since there is information loss for readability purposes. Use TransactionRaw in case you need exactly same encoding export const Transaction = P.wrap({ encodeStream: (w: P.Writer, value: Tx) => { const { msg, signatures } = value; const accounts: Record<string, { sign: boolean; write: boolean }> = {}; const add = (address: string, sign: boolean, write: boolean) => { let acc = accounts[address] || (accounts[address] = { sign: false, write: false }); acc.write ||= write; acc.sign ||= sign; }; add(msg.feePayer, true, true); for (let i of msg.instructions) for (let k of i.keys) add(k.address, k.sign, k.write); // Same loop as above, but cannot be merged since it will change implementation specific key positions inside transaction. // This doesn't invalidate transaction, but can be used for fingerprinting. for (let i of msg.instructions) add(i.program, false, false); const _keys = Object.keys(accounts); // [feePayer, ...sign+write, ...sign+read, ...nosign+write, ...nosign+read] const keys = [ msg.feePayer, ..._keys.filter((i) => accounts[i].sign && accounts[i].write && i !== msg.feePayer), ..._keys.filter((i) => accounts[i].sign && !accounts[i].write), ..._keys.filter((i) => !accounts[i].sign && accounts[i].write), ..._keys.filter((i) => !accounts[i].sign && !accounts[i].write), ]; let requiredSignatures = 0; let readSigned = 0; let readUnsigned = 0; for (let k of keys) { if (accounts[k].sign) requiredSignatures++; if (accounts[k].write) continue; if (accounts[k].sign) readSigned++; else readUnsigned++; } TransactionRaw.encodeStream(w, { signatures: keys .filter((i) => accounts[i].sign) .map((i) => signatures[i] || new Uint8Array(64)), msg: { requiredSignatures, readSigned, readUnsigned, keys, // indexOf potentially can be slow, but for most tx there will be ~3-5 keys, so doesn't matter much instructions: msg.instructions.map((i) => ({ programIdx: keys.indexOf(i.program), keys: i.keys.map((j) => keys.indexOf(j.address)), data: i.data, })), blockhash: msg.blockhash, }, }); }, decodeStream: (r: P.Reader): Tx => { const { signatures, msg } = TransactionRaw.decodeStream(r); if (signatures.length !== msg.requiredSignatures) throw new Error('SOL.tx: wrong signatures length'); if (msg.keys.length < signatures.length) throw new Error('SOL.tx: invalid keys length'); const sigs: Tx['signatures'] = {}; for (let i = 0; i < signatures.length; i++) sigs[msg.keys[i]] = signatures[i]; let accounts: Account[] = []; for (let i = 0; i < msg.keys.length; i++) { accounts.push({ address: msg.keys[i], ...keyParams(i, msg.requiredSignatures, msg.readSigned, msg.readUnsigned, msg.keys.length), }); } if (!accounts.length) throw new Error('SOL.tx: empty accounts array'); return { msg: { feePayer: accounts[0].address, blockhash: msg.blockhash, instructions: msg.instructions.map((i) => ({ program: accounts[i.programIdx].address, keys: i.keys.map((j) => accounts[j]), data: i.data, })), }, signatures: sigs, }; }, }); type KeyOpt = { sign: boolean; write: boolean; address?: string }; // Sort of ABI stuff, which allows to define encode/decode for programs easily type Method<T, K extends Record<string, KeyOpt>> = { coder: P.BytesCoder<T>; keys: K; }; export type TokenInfo = { symbol: string; decimals: number; price?: number; }; export type TokenList = Record<string, TokenInfo>; type MethodHint<T extends Method<any, any>> = T & { hint?: (o: MethodData<T>, t: TokenList) => string; }; // Remove keys with value 'never' type FilterKeys<T> = Pick<T, { [K in keyof T]: T[K] extends never ? never : K }[keyof T]>; type MethodData<T extends Method<any, any>> = P.UnwrapCoder<T['coder']> & FilterKeys<{ [A in keyof T['keys']]: T['keys'][A]['address'] extends string ? never : string }>; type Program<T extends Record<string, Method<any, any>>> = { [K in keyof T]: (data: MethodData<T[K]>) => Instruction; }; const registry: Record<string, (instr: Instruction, tl: TokenList) => MethodData<any>> = {}; // Basic ABI thing. There is IDL which is kinda ABI, but not official and system accounts doesn't have offical types for it. // Later we can add support to conversion IDL -> defineProgram export function defineProgram<T extends Record<string, MethodHint<any>>>( address: string, tagType: P.CoderType<number>, methods: T ): Program<T> { if (registry[address]) throw new Error('SOL: program for this address already defined'); const variants = P.map( tagType, Object.keys(methods).reduce((acc, k, i) => ({ ...acc, [k]: i }), {}) ); const coders: any = Object.keys(methods).reduce( (acc, k) => ({ ...acc, [k]: methods[k].coder }), {} ); const mainCoder = P.tag(variants, coders); registry[address] = (instr: Instruction, tl: TokenList): MethodData<any> => { if (instr.program !== address) throw new Error('SOL.parseInstruction: Wrong instruction program address'); const { TAG, data } = mainCoder.decode(instr.data); // Should be close to node parser (https://github.com/solana-labs/solana/tree/master/transaction-status/src) const res: Record<string, any> = { type: TAG, info: data }; const keys = Object.keys(methods[TAG].keys); if (keys.length !== instr.keys.length) throw new Error('SOL.parseInstruction: Keys length mismatch'); for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (methods[TAG].keys[key].address) { if (methods[TAG].keys[key].address !== instr.keys[i].address) { throw new Error( `SOL.parseInstruction(${address}/${TAG}): Invalid constant address for key exp=${methods[TAG].keys[key].address} got=${instr.keys[i].address}` ); } continue; } res.info[keys[i]] = instr.keys[i].address; } if (methods[TAG].hint) res.hint = methods[TAG].hint(data, tl); return res as MethodData<any>; }; const program: Program<T> = {} as Program<T>; for (const m in methods) { program[m] = (data: MethodData<(typeof methods)[typeof m]>): Instruction => ({ program: address, data: mainCoder.encode({ TAG: m, data }), keys: Object.keys(methods[m].keys).map((name) => { let { sign, write, address } = methods[m].keys[name]; address ||= (data as any)[name]; validateAddress(address); return { address, sign, write }; }), }); } return program; } export function parseInstruction(instr: Instruction, tl: TokenList): any { if (!registry[instr.program]) return; return registry[instr.program](instr, tl); } export const SYS_RECENT_BLOCKHASHES = 'SysvarRecentB1ockHashes11111111111111111111'; export const SYS_RENT = 'SysvarRent111111111111111111111111111111111'; export const SYS_PROGRAM = '11111111111111111111111111111111'; export const sys = defineProgram(SYS_PROGRAM, P.U32LE, { createAccount: { coder: P.struct({ lamports: P.U64LE, space: P.U64LE, owner: pubKey }), keys: { source: { sign: true, write: true }, newAccount: { sign: true, write: true }, }, hint: (o: { source: string; newAccount: string; lamports: bigint; space: bigint; owner: string; }) => `Create new account=${o.newAccount} with balance of ${Decimal.encode( o.lamports )} and owner program ${o.owner}, using funding account ${o.source}`, }, assign: { coder: P.struct({ owner: pubKey }), keys: { account: { sign: true, write: true } }, hint: (o: { account: string; owner: string }) => `Assign account=${o.account} to owner program=${o.owner}`, }, transfer: { coder: P.struct({ lamports: P.U64LE }), keys: { source: { sign: true, write: true }, destination: { sign: false, write: true } }, hint: (o: { lamports: bigint; source: string; destination: string }) => `Transfer ${Decimal.encode(o.lamports)} SOL from ${o.source} to ${o.destination}`, }, createAccountWithSeed: { coder: P.struct({ base: pubKey, seed: rustString, lamports: P.U64LE, space: P.U64LE, owner: pubKey, }), keys: { source: { sign: true, write: true }, newAccount: { sign: false, write: true }, base: { sign: true, write: false }, }, }, advanceNonce: { coder: P.struct({}), keys: { nonceAccount: { sign: false, write: true }, _recent_bh: { address: SYS_RECENT_BLOCKHASHES, sign: false, write: false }, nonceAuthority: { sign: true, write: false }, }, hint: (o: { nonceAccount: string; nonceAuthority: string }) => `Consume nonce in nonce account=${o.nonceAccount} (owner: ${o.nonceAuthority})`, }, withdrawFromNonce: { coder: P.struct({ lamports: P.U64LE }), keys: { nonceAccount: { sign: false, write: true }, destination: { sign: false, write: true }, _recent_bh: { address: SYS_RECENT_BLOCKHASHES, sign: false, write: false }, _rent: { address: SYS_RENT, sign: false, write: false }, nonceAuthority: { sign: true, write: false }, }, hint: (o: { lamports: bigint; destination: string; nonceAccount: string; nonceAuthority: string; }) => `Withdraw ${Decimal.encode(o.lamports)} SOL from nonce account=${o.nonceAccount} (owner: ${ o.nonceAuthority }) to ${o.destination}`, }, initializeNonce: { coder: P.struct({ nonceAuthority: pubKey }), keys: { nonceAccount: { sign: false, write: true }, _recent_bh: { address: SYS_RECENT_BLOCKHASHES, sign: false, write: false }, _rent: { address: SYS_RENT, sign: false, write: false }, }, }, authorizeNonce: { coder: P.struct({ newAuthorized: pubKey }), keys: { nonceAccount: { sign: false, write: true }, nonceAuthority: { sign: true, write: false }, }, hint: (o: { nonceAccount: string; nonceAuthority: string; newAuthorized: string }) => `Change owner of nonce account=${o.nonceAccount} from ${o.nonceAuthority} to ${o.newAuthorized}`, }, allocate: { coder: P.struct({ space: P.U64LE }), keys: { account: { sign: true, write: true }, }, }, allocateWithSeed: { coder: P.struct({ base: pubKey, seed: rustString, space: P.U64LE, owner: pubKey, }), keys: { account: { sign: false, write: true }, base: { sign: true, write: false }, }, }, assignWithSeed: { coder: P.struct({ base: pubKey, seed: rustString, owner: pubKey, }), keys: { account: { sign: false, write: true }, base: { sign: true, write: false }, }, }, transferWithSeed: { coder: P.struct({ lamports: P.U64LE, sourceSeed: rustString, sourceOwner: pubKey, }), keys: { source: { sign: false, write: true }, sourceBase: { sign: true, write: false }, destination: { sign: false, write: true }, }, }, }); // Type tests const assertType = <T>(_value: T) => {}; assertType<(o: { lamports: bigint; source: string; destination: string }) => Instruction>( sys.transfer ); assertType<(o: { lamports: bigint; nonceAccount: string; nonceAuthority: string }) => Instruction>( sys.advanceNonce ); const authorityType = P.map(P.U8, { MintTokens: 0, FreezeAccount: 1, AccountOwner: 2, CloseAccount: 3, }); const tokenName = (address: string, tl: TokenList) => tl[address]?.symbol || address; export const TOKEN_PROGRAM = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'; export const token = defineProgram(TOKEN_PROGRAM, P.U8, { initializeMint: { coder: P.struct({ decimals: P.U8, mintAuthority: pubKey, freezeAuthority: P.optional(P.bool, pubKey, '11111111111111111111111111111111'), }), keys: { mint: { sign: false, write: true }, _rent: { address: SYS_RENT, sign: false, write: false }, }, }, initializeAccount: { coder: P.struct({}), keys: { account: { sign: false, write: true }, mint: { sign: false, write: false }, owner: { sign: false, write: false }, _rent: { address: SYS_RENT, sign: false, write: false }, }, hint: (o: { owner: string; account: string; mint: string }, tl: TokenList) => `Initialize token account=${o.account} with owner=${o.owner} token=${tokenName(o.mint, tl)}`, }, // TODO: multisig support? initializeMultisig: { coder: P.struct({ m: P.U8 }), keys: { account: { sign: false, write: true }, _rent: { address: SYS_RENT, sign: false, write: false }, }, hint: (o: { account: string; m: number }, _: TokenList) => `Initialize multi-sig token account=${o.account} with signatures=${o.m}`, }, transfer: { coder: P.struct({ amount: P.U64LE }), keys: { source: { sign: false, write: true }, destination: { sign: false, write: true }, owner: { sign: true, write: false }, }, hint: ( o: { amount: bigint; source: string; destination: number; owner: string }, _: TokenList ) => `Transfer ${o.amount} from token account=${o.source} of owner=${o.owner} to ${o.destination}`, }, approve: { coder: P.struct({ amount: P.U64LE }), keys: { account: { sign: false, write: true }, delegate: { sign: false, write: false }, owner: { sign: true, write: false }, }, hint: (o: { amount: bigint; account: string; delegate: number; owner: string }, _: TokenList) => `Approve authority of delegate=${o.delegate} over tokens on account=${o.account} on behalf of owner=${o.owner}`, }, revoke: { coder: P.struct({}), keys: { account: { sign: false, write: true }, owner: { sign: true, write: false }, }, hint: (o: { amount: bigint; account: string; owner: string }, _: TokenList) => `Revoke delegate's authority over tokens on account=${o.account} on behalf of owner=${o.owner}`, }, setAuthority: { coder: P.struct({ authorityType, newAuthority: P.optional(P.bool, pubKey, '11111111111111111111111111111111'), }), keys: { account: { sign: false, write: true }, currentAuthority: { sign: true, write: false }, }, hint: ( o: { newAuthority: string; account: string; currentAuthority: string; authorityType: string }, _: TokenList ) => `Sets a new authority=${o.newAuthority} of a mint or account=${o.account}. Current authority=${o.currentAuthority}. Authority Type: ${o.authorityType}`, }, mintTo: { coder: P.struct({ amount: P.U64LE }), keys: { mint: { sign: false, write: true }, dest: { sign: false, write: true }, authority: { sign: true, write: false }, }, }, burn: { coder: P.struct({ amount: P.U64LE }), keys: { account: { sign: false, write: true }, mint: { sign: false, write: true }, owner: { sign: true, write: false }, }, hint: (o: { amount: bigint; account: string; mint: string; owner: string }, _: TokenList) => `Burn ${o.amount} tokens from account=${o.account} of owner=${o.owner} mint=${o.mint}`, }, closeAccount: { coder: P.struct({}), keys: { account: { sign: false, write: true }, dest: { sign: false, write: true }, owner: { sign: true, write: false }, }, hint: (o: { account: string; dest: string; owner: string }, _: TokenList) => `Close token account=${o.account} of owner=${o.owner}, transferring all its SOL to destionation account=${o.dest}`, }, freezeAccount: { coder: P.struct({}), keys: { account: { sign: false, write: true }, mint: { sign: false, write: true }, authority: { sign: true, write: false }, }, hint: (o: { account: string; authority: string; mint: string }, _: TokenList) => `Freeze token account=${o.account} of mint=${o.mint} using freeze_authority=${o.authority}`, }, thawAccount: { coder: P.struct({}), keys: { account: { sign: false, write: true }, mint: { sign: false, write: false }, authority: { sign: true, write: false }, }, hint: (o: { account: string; authority: string; mint: string }, _: TokenList) => `Thaw a frozne token account=${o.account} of mint=${o.mint} using freeze_authority=${o.authority}`, }, transferChecked: { coder: P.struct({ amount: P.U64LE, decimals: P.U8 }), keys: { source: { sign: false, write: true }, mint: { sign: false, write: false }, destination: { sign: false, write: true }, owner: { sign: true, write: false }, }, hint: ( o: { amount: bigint; source: string; destination: number; owner: string; decimals: number; mint: string; }, tl: TokenList ) => `Transfer ${P.coders.decimal(o.decimals).encode(o.amount)} ${tokenName( o.mint, tl )} from token account=${o.source} of owner=${o.owner} to ${o.destination}`, }, approveChecked: { coder: P.struct({ amount: P.U64LE, decimals: P.U8 }), keys: { source: { sign: false, write: true }, mint: { sign: false, write: false }, delegate: { sign: false, write: false }, owner: { sign: true, write: false }, }, hint: ( o: { amount: bigint; source: string; delegate: number; owner: string; decimals: number; mint: string; }, tl: TokenList ) => `Approve delgate=${o.delegate} authority on behalf account=${o.source} owner=${ o.owner } over ${P.coders.decimal(o.decimals).encode(o.amount)} ${tokenName(o.mint, tl)}`, }, mintToChecked: { coder: P.struct({ amount: P.U64LE, decimals: P.U8 }), keys: { mint: { sign: false, write: true }, dest: { sign: false, write: true }, authority: { sign: true, write: false }, }, hint: ( o: { amount: bigint; dest: string; authority: string; mint: string; decimals: number; }, tl: TokenList ) => `Mint new tokens (${P.coders.decimal(o.decimals).encode(o.amount)} ${tokenName( o.mint, tl )}) to account=${o.dest} using authority=${o.authority}`, }, burnChecked: { coder: P.struct({ amount: P.U64LE, decimals: P.U8 }), keys: { mint: { sign: false, write: true }, account: { sign: false, write: true }, owner: { sign: true, write: false }, }, hint: ( o: { amount: bigint; account: string; owner: string; mint: string; decimals: number; }, tl: TokenList ) => `Burn tokens (${P.coders.decimal(o.decimals).encode(o.amount)} ${tokenName( o.mint, tl )}) on account=${o.account} of owner=${o.owner}`, }, initializeAccount2: { coder: P.struct({ owner: pubKey }), keys: { account: { sign: false, write: true }, mint: { sign: false, write: false }, _rent: { address: SYS_RENT, sign: false, write: false }, }, hint: (o: { owner: string; account: string; mint: string }, tl: TokenList) => `Initialize token account=${o.account} with owner=${o.owner} token=${tokenName(o.mint, tl)}`, }, syncNative: { coder: P.struct({}), keys: { nativeAccount: { sign: false, write: true } }, hint: (o: { nativeAccount: string }) => `Sync SOL balance for wrapped account ${o.nativeAccount}`, }, }); export const NonceAccount = P.struct({ version: P.U32LE, state: P.U32LE, authority: pubKey, nonce: pubKey, lamportPerSignature: P.U64LE, }); function mod(a: bigint, b: bigint = ed25519.CURVE.Fp.ORDER) { const res = a % b; return res >= 0n ? res : b + res; } export function isOnCurve(bytes: Bytes | string) { if (typeof bytes === 'string') bytes = base58.decode(bytes); try { // noble-ed25519 checks that publicKey is < P, but dalek (ed25519-dalek.CompressedEdwardsY) is not, so we do modulo here. // first bit in last byte is x oddity flag const last = bytes[31]; const normedLast = last & ~0x80; const normed = Uint8Array.from(Array.from(bytes.slice(0, 31)).concat(normedLast)); const modBytes = P.U256LE.encode(mod(P.U256LE.decode(normed))); if ((last & 0x80) !== 0) modBytes[31] |= 0x80; ed25519.ExtendedPoint.fromHex(modBytes); return true; } catch (e) { return false; } } export function programAddress(program: string, ...seeds: Bytes[]) { let seed = P.utils.concatBytes(...seeds); const noncePos = seed.length; seed = P.utils.concatBytes( seed, new Uint8Array([0]), base58.decode(program), utf8.decode('ProgramDerivedAddress') ); for (let i = 255; i >= 0; i--) { seed[noncePos] = i; const hash = sha256(seed); if (isOnCurve(hash)) continue; return base58.encode(hash); } throw new Error('SOL.programAddress: nonce exhausted, cannot find program address'); } export const ASSOCIATED_TOKEN_PROGRAM = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'; export const associatedToken = defineProgram(ASSOCIATED_TOKEN_PROGRAM, P.constant(0), { create: { coder: P.struct({}), keys: { source: { sign: true, write: true }, account: { sign: false, write: true }, wallet: { sign: false, write: false }, mint: { sign: false, write: false }, _sys: { address: SYS_PROGRAM, sign: false, write: false }, _token: { address: TOKEN_PROGRAM, sign: false, write: false }, _rent: { address: SYS_RENT, sign: false, write: false }, }, hint: (o: { account: string; wallet: string; mint: string; source: string }, tl: TokenList) => `Initialize associated token account=${o.account} with owner=${ o.wallet } for token=${tokenName(o.mint, tl)}, payed by ${o.source}`, }, }); export function tokenAddress(mint: string, owner: string, allowOffCurveOwner = false) { if (!allowOffCurveOwner && !isOnCurve(owner)) throw new Error('Owner is off curve (cannot sign)'); return programAddress( ASSOCIATED_TOKEN_PROGRAM, ...[owner, TOKEN_PROGRAM, mint].map((i) => base58.decode(i)) ); } // https://raw.githubusercontent.com/solana-labs/token-list/main/src/tokens/solana.tokenlist.json export const COMMON_TOKENS: TokenList = { So11111111111111111111111111111111111111112: { decimals: 9, symbol: 'SOL' }, // Wrapped SOL Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB: { decimals: 6, symbol: 'USDT', price: 1 }, EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: { decimals: 6, symbol: 'USDC', price: 1 }, }; export function tokenFromSymbol(symbol: string, tokens = COMMON_TOKENS) { for (let c in tokens) if (tokens[c].symbol === symbol) return { ...tokens[c], contract: c }; return; } // [1, 0, 0, 0] -> true // [0, 0, 0, 0] -> false const U32LEBOOL = P.padRight(4, P.bool, () => 0); export const TokenAccount = P.struct({ mint: pubKey, owner: pubKey, amount: P.U64LE, delegate: P.optional(U32LEBOOL, pubKey, '11111111111111111111111111111111'), state: P.map(P.U8, { uninitialized: 0, initialized: 1, frozen: 2, }), isNative: P.optional(U32LEBOOL, P.U64LE, 0n), delegateAmount: P.U64LE, closeAuthority: P.optional(U32LEBOOL, pubKey, '11111111111111111111111111111111'), }); export const swapProgram = 'SwaPpA9LAaLfeLi3a68M4DjnLqgtticKg6CnyNwgAC8'; type TxData = Bytes | string; export function verifyTx(tx: TxData) { if (typeof tx === 'string') tx = base64.decode(tx); if (tx.length > 1280 - 40 - 8) throw new Error('sol: transaction too big'); const parsed = Transaction.decode(tx); const raw = TransactionRaw.decode(tx); const msg = Message.encode(TransactionRaw.decode(tx).msg); for (let i = 0; i < raw.msg.requiredSignatures; i++) { const address = raw.msg.keys[i]; const pubKey = base58.decode(address); const sig = parsed.signatures[address]; if (!ed25519.verify(sig, msg, pubKey)) throw new Error(`sol: invalid signature sig=${sig} msg=${msg}`); } } export function getPublicKey(privateKey: Bytes) { return ed25519.getPublicKey(privateKey); } export function getAddress(privateKey: Bytes) { const publicKey = getPublicKey(privateKey); return base58.encode(publicKey); } export function getAddressFromPublicKey(publicKey: Bytes) { return base58.encode(publicKey); } type PrivateKeyFormat = 'base58' | 'hex' | 'array'; export function formatPrivate(privateKey: Bytes, format: PrivateKeyFormat = 'base58') { const publicKey = getPublicKey(privateKey); const fullKey = P.utils.concatBytes(privateKey, publicKey); switch (format) { case 'base58': { return base58.encode(fullKey); } case 'hex': { return hex.encode(fullKey); } case 'array': { return Array.from(fullKey); } default: { throw new Error('sol: unsupported format'); } } } export function createTxComplex(address: string, instructions: Instruction[], blockhash: string) { if (!instructions.length) throw new Error('SOLPublic: empty instructions array'); return base64.encode( Transaction.encode({ msg: { feePayer: address, blockhash, instructions }, signatures: {}, }) ); } export function createTx( from: string, to: string, amount: string, _fee: bigint, blockhash: string ) { const amountNum = Decimal.decode(amount); return createTxComplex( from, [sys.transfer({ source: from, destination: to, lamports: amountNum })], blockhash ); } export function signTx(privateKey: Bytes, data: TxData): [string, string] { if (typeof data === 'string') data = base64.decode(data); const address = getAddress(privateKey); const raw = TransactionRaw.decode(data); const reqSignatures = raw.msg.keys.slice(0, raw.msg.requiredSignatures); if (!reqSignatures.filter((i) => i == address).length) throw new Error(`SOLPrivate: tx doesn't require signature for address=${address}`); const sig = ed25519.sign(Message.encode(raw.msg), privateKey); for (let i = 0; i < reqSignatures.length; i++) if (reqSignatures[i] === address) raw.signatures[i] = sig; // Base58 encoding for tx is deprecated const tx = base64.encode(TransactionRaw.encode(raw)); // first signature is txHash return [base58.encode(sig), tx]; }