micro-sol-signer
Version:
Create, sign & decode Solana transactions with minimum deps
516 lines • 22.6 kB
JavaScript
;
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