UNPKG

micro-sol-signer

Version:

Create, sign & decode Solana transactions with minimum deps

516 lines 22.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CONTRACTS = exports.programAddress = exports.isOnCurve = exports.AddressTableLookupData = exports.TokenAccount = exports.tokenAddress = exports.ASSOCIATED_TOKEN_PROGRAM = exports.TOKEN_PROGRAM2022 = exports.TOKEN_PROGRAM = exports.SYS_PROGRAM = exports.associatedToken = exports.token2022 = exports.token = exports.sys = exports.PROGRAMS = exports.Transaction = exports.TransactionRaw = exports.Message = exports.MessageRaw = exports.shortU16 = exports.pubKey = exports.PRECISION = exports.Decimal = exports.Offchain = void 0; exports.validateAddress = validateAddress; exports.AddressLookupTables = AddressLookupTables; exports.decodeAccount = decodeAccount; exports.parseInstruction = parseInstruction; exports.verifyTx = verifyTx; exports.getPublicKey = getPublicKey; exports.getAddress = getAddress; exports.formatPrivate = formatPrivate; exports.formatPublic = formatPublic; exports.parseAddress = parseAddress; exports.createTx = createTx; exports.createTransferSol = createTransferSol; exports.createTokenTransfer = createTokenTransfer; exports.createTokenTransferChecked = createTokenTransferChecked; exports.signTx = signTx; exports.signBytes = signBytes; exports.verifyBytes = verifyBytes; exports.getMessageFromTransaction = getMessageFromTransaction; const ed25519_1 = require("@noble/curves/ed25519"); const base_1 = require("@scure/base"); const P = require("micro-packed"); const idl = require("./idl/index.js"); const index_ts_1 = require("./idl/index.js"); Object.defineProperty(exports, "Decimal", { enumerable: true, get: function () { return index_ts_1.Decimal; } }); Object.defineProperty(exports, "PRECISION", { enumerable: true, get: function () { return index_ts_1.PRECISION; } }); Object.defineProperty(exports, "pubKey", { enumerable: true, get: function () { return index_ts_1.pubKey; } }); Object.defineProperty(exports, "shortU16", { enumerable: true, get: function () { return index_ts_1.shortU16; } }); // System: solana IDLs const alt_ts_1 = require("./idl/alt.js"); const computeBudget_ts_1 = require("./idl/computeBudget.js"); const config_ts_1 = require("./idl/config.js"); const memo_ts_1 = require("./idl/memo.js"); const system_ts_1 = require("./idl/system.js"); const token_ts_1 = require("./idl/token.js"); const token2022_ts_1 = require("./idl/token2022.js"); var offchain_ts_1 = require("./offchain.js"); Object.defineProperty(exports, "Offchain", { enumerable: true, get: function () { return offchain_ts_1.Offchain; } }); const MAX_TX_SIZE = 1280 - 40 - 8; function removeUndefined(obj) { if (obj === null || obj === undefined) return obj; if (typeof obj !== 'object') return obj; if (Array.isArray(obj)) return obj.map((item) => removeUndefined(item)); const res = {}; for (const [key, value] of Object.entries(obj)) { if (value === undefined) continue; res[key] = removeUndefined(value); } return res; } function validateAddress(address) { const pubkey = base_1.base58.decode(address); if (pubkey.length !== 32) throw new Error('Invalid Solana address'); } const keyParams = (i, req, signed, unsigned, total) => ({ sign: i < req ? true : false, write: i < req - signed || (i >= req && i < total - unsigned) ? true : false, }); const MessageHeader = P.struct({ requiredSignatures: P.U8, readSigned: P.U8, readUnsigned: P.U8, }); const Instruction = P.struct({ programIdx: P.U8, keys: P.array(index_ts_1.shortU16, P.U8), data: P.bytes(index_ts_1.shortU16), }); const MessageLegacy = P.struct({ header: MessageHeader, keys: P.array(index_ts_1.shortU16, index_ts_1.pubKey), blockhash: index_ts_1.pubKey, instructions: P.array(index_ts_1.shortU16, Instruction), }); const MessageAddressTableLookup = P.struct({ account: index_ts_1.pubKey, writableIndexes: P.array(index_ts_1.shortU16, P.U8), readonlyIndexes: P.array(index_ts_1.shortU16, P.U8), }); const MessageV0 = P.struct({ header: MessageHeader, keys: P.array(index_ts_1.shortU16, index_ts_1.pubKey), blockhash: index_ts_1.pubKey, instructions: P.array(index_ts_1.shortU16, Instruction), ALT: P.array(index_ts_1.shortU16, MessageAddressTableLookup), }); const MessageVersion = P.wrap({ encodeStream(w, value) { if (value === 'legacy') { // legacy is empty! } else if (typeof value === 'number') { if (value < 0 || value > 127) throw new Error('Invalid message version'); w.byte(0x80 | value); } else throw new Error('Invalid message version type'); }, decodeStream(r) { const b = r.byte(true); if ((b & 0x80) === 0) return 'legacy'; r.byte(); // move cursor return b & 0x7f; }, }); exports.MessageRaw = P.tag(MessageVersion, { legacy: MessageLegacy, 0: MessageV0, }); const getAccountKeys = (msg) => { const accounts = []; for (let i = 0; i < msg.data.keys.length; i++) { accounts.push({ address: msg.data.keys[i], ...keyParams(i, msg.data.header.requiredSignatures, msg.data.header.readSigned, msg.data.header.readUnsigned, msg.data.keys.length), }); } if (!accounts.length) throw new Error('SOL.tx: empty accounts array'); if (msg.TAG !== 'legacy') { for (const alt of msg.data.ALT) { for (const idx of alt.writableIndexes) accounts.push({ address: `${alt.account}:${idx}`, write: true, sign: false }); } for (const alt of msg.data.ALT) { for (const idx of alt.readonlyIndexes) accounts.push({ address: `${alt.account}:${idx}`, write: false, sign: false }); } } return accounts; }; const MessageCoder = { encode(msg) { const accounts = getAccountKeys(msg); return { version: msg.TAG, feePayer: accounts[0].address, blockhash: msg.data.blockhash, instructions: msg.data.instructions.map((i) => ({ program: accounts[i.programIdx].address, keys: i.keys.map((j) => accounts[j]), data: i.data, })), }; }, decode(to) { const { version, feePayer, blockhash, instructions } = to; const accounts = new Map(); // contract -> idx -> isWrite const ALTaccounts = {}; const add = (address, sign, write) => { if (address.includes(':')) { if (version === 'legacy') throw new Error('SOL.tx: cannot use AddressLookupTable addresses in legacy tx'); if (sign) throw new Error('SOL.tx: cannot sign with address for AddressLookupTable'); const [contract, idx] = address.split(':'); if (!ALTaccounts[contract]) ALTaccounts[contract] = new Map(); // JS quirk: Object keys is always insert order unless they are "numeric" (even if string!) // so '1' will always be on top, breaking insert order guarantess and introducing fingerprinting in tx // This also breaks encode(decode). Fortunately we have Map-s if (!ALTaccounts[contract].has(idx)) ALTaccounts[contract].set(idx, write); return; } if (!accounts.has(address)) accounts.set(address, { sign: false, write: false }); const acc = accounts.get(address); acc.write || (acc.write = write); acc.sign || (acc.sign = sign); }; add(feePayer, true, true); for (const i of instructions) { add(i.program, false, false); for (let k of i.keys) add(k.address, k.sign, k.write); } const _keys = Array.from(accounts.keys()); // [feePayer, ...sign+write, ...sign+read, ...nosign+write, ...nosign+read] const keys = [ feePayer, ..._keys.filter((i) => accounts.get(i).sign && accounts.get(i).write && i !== feePayer), ..._keys.filter((i) => accounts.get(i).sign && !accounts.get(i).write), ..._keys.filter((i) => !accounts.get(i).sign && accounts.get(i).write), ..._keys.filter((i) => !accounts.get(i).sign && !accounts.get(i).write), ]; let requiredSignatures = 0; let readSigned = 0; let readUnsigned = 0; for (let k of keys) { if (accounts.get(k).sign) requiredSignatures++; if (accounts.get(k).write) continue; if (accounts.get(k).sign) readSigned++; else readUnsigned++; } const header = { requiredSignatures, readSigned, readUnsigned }; const ALT = []; if (version !== 'legacy') { const contractNames = Object.keys(ALTaccounts).sort(); for (const account of contractNames) { const writableIndexes = []; const readonlyIndexes = []; for (const k of ALTaccounts[account].keys()) { (ALTaccounts[account].get(k) ? writableIndexes : readonlyIndexes).push(+k); } ALT.push({ account, writableIndexes, readonlyIndexes }); } } const accountKeys = getAccountKeys({ TAG: version, data: { header, keys, ALT } }); const accountMap = Object.fromEntries(accountKeys.map((i, j) => [i.address, j])); const getKey = (address) => { const value = accountMap[address]; if (value === undefined) throw new Error('SOL.tx: unknown address: ' + address); return value; }; return { TAG: version, data: { header, keys, instructions: instructions.map((i) => ({ programIdx: getKey(i.program), keys: i.keys.map((i) => getKey(i.address)), data: i.data, })), blockhash, ALT: ALT, }, }; }, }; exports.Message = P.apply(exports.MessageRaw, MessageCoder); exports.TransactionRaw = P.struct({ signatures: P.array(index_ts_1.shortU16, P.bytes(64)), msg: exports.MessageRaw, }); exports.Transaction = P.apply(exports.TransactionRaw, { encode(from) { const { signatures, msg } = from; if (signatures.length !== msg.data.header.requiredSignatures) throw new Error('SOL.tx: not enough signatures'); return { signatures: Object.fromEntries(signatures.map((i, j) => [msg.data.keys[j], i])), msg: MessageCoder.encode(msg), }; }, decode(to) { const raw = MessageCoder.decode(to.msg); const signatures = []; for (let i = 0; i < raw.data.header.requiredSignatures; i++) { const address = raw.data.keys[i]; const sig = to.signatures[address]; // NOTE: this will break on unsigned transactions! Where we can check this? // if (sig === undefined) throw new Error('SOL.tx: missing signature for address: ' + address); signatures.push(sig === undefined ? new Uint8Array(64) : sig); } return { signatures, msg: raw }; }, }); // Tables is like {contract: [addr1, addr2]} (from archive.getAddressLookupTable().addresses) function AddressLookupTables(tables) { // XXX:1 -> YYY const direct = new Map(); // YYY -> XXX:1 const reverse = new Map(); for (const k in tables) { const t = tables[k]; for (let i = 0; i < t.length; i++) { const contract = `${k}:${i}`; const address = t[i]; direct.set(contract, address); // Order of contracts == priority if (!reverse.has(address)) reverse.set(address, contract); } } const mapInstructions = (tx, fn) => { const instructions = tx.msg.instructions.map((i) => ({ program: fn(i.program), keys: i.keys.map((j) => ({ ...j, address: fn(j.address) })), data: i.data, })); return { signatures: tx.signatures, msg: { ...tx.msg, instructions } }; }; return { // resolve addresses in transaction using provided tables resolve: (tx) => mapInstructions(tx, (k) => (direct.has(k) ? direct.get(k) : k)), // compresses addresses using tables compress(tx) { const blacklist = new Set(); blacklist.add(tx.msg.feePayer); for (const i of tx.msg.instructions) { for (const k of i.keys) if (k.sign) blacklist.add(k.address); } return mapInstructions(tx, (k) => !reverse.has(k) || blacklist.has(k) ? k : reverse.get(k)); }, }; } exports.PROGRAMS = { ...idl.defineIDL(system_ts_1.default), ...idl.defineIDL(token_ts_1.default), ...idl.defineIDL(token2022_ts_1.default), ...idl.defineIDL(alt_ts_1.default), ...idl.defineIDL(computeBudget_ts_1.default), ...idl.defineIDL(config_ts_1.default), ...idl.defineIDL(memo_ts_1.default), }; // Old API compat exports.sys = exports.PROGRAMS.system.program.instructions.encoders; exports.token = exports.PROGRAMS.token.program.instructions.encoders; // TODO: The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed. exports.token2022 = exports.PROGRAMS['token-2022'].program.instructions.encoders; exports.associatedToken = exports.PROGRAMS.token.additionalPrograms.associatedToken.instructions.encoders; exports.SYS_PROGRAM = exports.PROGRAMS.system.program.contract; exports.TOKEN_PROGRAM = exports.PROGRAMS.token.program.contract; exports.TOKEN_PROGRAM2022 = exports.PROGRAMS['token-2022'].program.contract; exports.ASSOCIATED_TOKEN_PROGRAM = exports.PROGRAMS.token.additionalPrograms.associatedToken.contract; exports.tokenAddress = exports.PROGRAMS.token.additionalPrograms.associatedToken.pdas.associatedToken; exports.TokenAccount = exports.PROGRAMS.token.program.accounts.decoder; exports.AddressTableLookupData = exports.PROGRAMS.addressLookupTable.program.accounts.decoder; exports.isOnCurve = idl.isOnCurve; exports.programAddress = idl.programAddress; const TOKENS_ENCODE = { [exports.TOKEN_PROGRAM]: exports.PROGRAMS.token.program.instructions.encoders, [exports.TOKEN_PROGRAM2022]: exports.PROGRAMS['token-2022'].program.instructions.encoders, }; const ACCOUNTS_DECODE = { [exports.SYS_PROGRAM]: exports.PROGRAMS.system.program.accounts.decoder, [exports.TOKEN_PROGRAM]: exports.PROGRAMS.token.program.accounts.decoder, [exports.TOKEN_PROGRAM2022]: exports.PROGRAMS['token-2022'].program.accounts.decoder, [exports.ASSOCIATED_TOKEN_PROGRAM]: exports.PROGRAMS.token.additionalPrograms.associatedToken.accounts.decoder, [exports.PROGRAMS.addressLookupTable.program.contract]: exports.PROGRAMS.addressLookupTable.program.accounts.decoder, [exports.PROGRAMS.computeBudget.program.contract]: exports.PROGRAMS.computeBudget.program.accounts.decoder, [exports.PROGRAMS.solanaConfig.program.contract]: exports.PROGRAMS.solanaConfig.program.accounts.decoder, [exports.PROGRAMS.memo.program.contract]: exports.PROGRAMS.memo.program.accounts.decoder, }; function decodeAccount(contract, data) { if (ACCOUNTS_DECODE[contract] === undefined) throw new Error('unknown contract'); return removeUndefined(ACCOUNTS_DECODE[contract](data)); } const REGISTRY = { [exports.SYS_PROGRAM]: exports.PROGRAMS.system.program.instructions.decoder, [exports.TOKEN_PROGRAM]: exports.PROGRAMS.token.program.instructions.decoder, [exports.TOKEN_PROGRAM2022]: exports.PROGRAMS['token-2022'].program.instructions.decoder, [exports.ASSOCIATED_TOKEN_PROGRAM]: exports.PROGRAMS.token.additionalPrograms.associatedToken.instructions.decoder, [exports.PROGRAMS.addressLookupTable.program.contract]: exports.PROGRAMS.addressLookupTable.program.instructions.decoder, [exports.PROGRAMS.computeBudget.program.contract]: exports.PROGRAMS.computeBudget.program.instructions.decoder, [exports.PROGRAMS.solanaConfig.program.contract]: exports.PROGRAMS.solanaConfig.program.instructions.decoder, [exports.PROGRAMS.memo.program.contract]: exports.PROGRAMS.memo.program.instructions.decoder, }; function parseInstruction(instruction) { if (REGISTRY[instruction.program] === undefined) throw new Error('unknown contract'); return removeUndefined(REGISTRY[instruction.program](instruction)); } exports.CONTRACTS = { [exports.SYS_PROGRAM]: exports.PROGRAMS.system.program, [exports.TOKEN_PROGRAM]: exports.PROGRAMS.token.program, [exports.TOKEN_PROGRAM2022]: exports.PROGRAMS['token-2022'].program, [exports.ASSOCIATED_TOKEN_PROGRAM]: exports.PROGRAMS.token.additionalPrograms.associatedToken, [exports.PROGRAMS.addressLookupTable.program.contract]: exports.PROGRAMS.addressLookupTable.program, [exports.PROGRAMS.computeBudget.program.contract]: exports.PROGRAMS.computeBudget.program, [exports.PROGRAMS.solanaConfig.program.contract]: exports.PROGRAMS.solanaConfig.program, [exports.PROGRAMS.memo.program.contract]: exports.PROGRAMS.memo.program, }; function verifyTx(tx) { if (typeof tx === 'string') tx = base_1.base64.decode(tx); if (tx.length > MAX_TX_SIZE) throw new Error('sol: transaction too big'); const raw = exports.TransactionRaw.decode(tx); const msg = exports.MessageRaw.encode(raw.msg); for (let i = 0; i < raw.msg.data.header.requiredSignatures; i++) { const address = raw.msg.data.keys[i]; const pubKey = base_1.base58.decode(address); const sig = raw.signatures[i]; if (!ed25519_1.ed25519.verify(sig, msg, pubKey)) throw new Error(`sol: invalid signature sig=${sig} msg=${msg}`); } } function getPublicKey(privateKey) { return ed25519_1.ed25519.getPublicKey(privateKey); } function getAddress(privateKey) { const publicKey = getPublicKey(privateKey); return base_1.base58.encode(publicKey); } function formatPrivate(privateKey, format = 'base58') { const publicKey = getPublicKey(privateKey); const fullKey = P.utils.concatBytes(privateKey, publicKey); switch (format) { case 'base58': { return base_1.base58.encode(fullKey); } case 'hex': { return base_1.hex.encode(fullKey); } case 'array': { return Array.from(fullKey); } default: { throw new Error('sol: unsupported format'); } } } function formatPublic(publicKey) { return base_1.base58.encode(publicKey); } function parseAddress(address) { return base_1.base58.decode(address); } function createTx(address, instructions, blockhash, version = 0) { if (!instructions.length) throw new Error('SOLPublic: empty instructions array'); return base_1.base64.encode(exports.Transaction.encode({ msg: { version, feePayer: address, blockhash, instructions }, signatures: {}, })); } function createTransferSol(from, to, amount, blockhash, version = 0) { return createTx(from, [exports.sys.transferSol({ source: from, destination: to, amount })], blockhash, version); } function createTokenTransfer(mint, from, to, amount, blockhash, tokenProgram = exports.TOKEN_PROGRAM, version = 0) { if (TOKENS_ENCODE[tokenProgram] === undefined) throw new Error('unknown program'); return createTx(from, [ TOKENS_ENCODE[tokenProgram].transfer({ source: (0, exports.tokenAddress)({ mint, owner: from, tokenProgram, }), destination: (0, exports.tokenAddress)({ mint, owner: to, tokenProgram, }), authority: from, amount, }), ], blockhash, version); } function createTokenTransferChecked(mint, from, to, amount, decimals, blockhash, tokenProgram = exports.TOKEN_PROGRAM, version = 0) { if (TOKENS_ENCODE[tokenProgram] === undefined) throw new Error('unknown program'); return createTx(from, [ TOKENS_ENCODE[tokenProgram].transferChecked({ source: (0, exports.tokenAddress)({ mint, owner: from, tokenProgram, }), amount, decimals, mint, authority: from, destination: (0, exports.tokenAddress)({ mint, owner: to, tokenProgram, }), }), ], blockhash, version); } function signTx(privateKey, data) { if (typeof data === 'string') data = base_1.base64.decode(data); const address = getAddress(privateKey); const raw = exports.TransactionRaw.decode(data); const reqSignatures = raw.msg.data.keys.slice(0, raw.msg.data.header.requiredSignatures); if (!reqSignatures.filter((i) => i == address).length) throw new Error(`SOLPrivate: tx doesn't require signature for address=${address}`); const sig = ed25519_1.ed25519.sign(exports.MessageRaw.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 = base_1.base64.encode(exports.TransactionRaw.encode(raw)); // first signature is txHash return [base_1.base58.encode(sig), tx]; } /** * Warning: It is NOT secure to sign random msgs, * because someone can create a message which is an encoded transaction. */ function signBytes(privateKey, msg) { return base_1.base58.encode(ed25519_1.ed25519.sign(msg, privateKey)); } function verifyBytes(sigature, publicKey, msg) { if (typeof publicKey === 'string') publicKey = base_1.base58.decode(publicKey); return ed25519_1.ed25519.verify(base_1.base58.decode(sigature), msg, publicKey); } function getMessageFromTransaction(tx) { const raw = exports.TransactionRaw.decode(base_1.base64.decode(tx)); return base_1.base64.encode(exports.MessageRaw.encode(raw.msg)); } //# sourceMappingURL=index.js.map