UNPKG

@stellar/stellar-sdk

Version:

A library for working with the Stellar network, including communication with the Horizon and Soroban RPC servers.

354 lines (351 loc) 12.9 kB
import { Buffer } from 'buffer'; import types from './generated/curr_generated.js'; import { Keypair } from './keypair.js'; import { StrKey } from './strkey.js'; import { hash } from './hashing.js'; import { Address } from './address.js'; import { nativeToScVal } from './scval.js'; function toBuffer(value) { if (value instanceof ArrayBuffer) { return Buffer.from(new Uint8Array(value)); } return Buffer.from(value); } async function authorizeEntry(entry, signer, validUntilLedgerSeq, networkPassphrase, forAddress) { if (entry.credentials().switch().value === types.SorobanCredentialsType.sorobanCredentialsSourceAccount().value) { return entry; } const clone = types.SorobanAuthorizationEntry.fromXDR(entry.toXDR()); const credentials = clone.credentials(); const addrAuth = getAddressCredentials(credentials); if (addrAuth === null) { throw new Error(`unsupported credential type ${credentials.switch().name}`); } addrAuth.signatureExpirationLedger(validUntilLedgerSeq); const preimage = buildAuthorizationEntryPreimage( clone, validUntilLedgerSeq, networkPassphrase ); const payload = hash(preimage.toXDR()); let signatureScVal; let targetAddress = forAddress; let sigResult = null; if (typeof signer === "function") { sigResult = await signer(preimage, Buffer.from(payload)); } if (sigResult !== null && typeof sigResult === "object" && "signatureScVal" in sigResult) { signatureScVal = sigResult.signatureScVal; targetAddress ??= sigResult.address; } else { let signature; let publicKey; if (typeof signer === "function") { if (sigResult !== null && typeof sigResult === "object" && "signature" in sigResult) { signature = toBuffer(sigResult.signature); publicKey = sigResult.publicKey; } else { signature = toBuffer(sigResult); publicKey = Address.fromScAddress(addrAuth.address()).toString(); } } else { signature = toBuffer(signer.sign(payload)); publicKey = signer.publicKey(); } if (!Keypair.fromPublicKey(publicKey).verify(payload, signature)) { throw new Error(`signature doesn't match payload`); } const sigScVal = nativeToScVal( { public_key: StrKey.decodeEd25519PublicKey(publicKey), signature }, { type: { public_key: ["symbol", null], signature: ["symbol", null] } } ); signatureScVal = types.ScVal.scvVec([sigScVal]); } const targets = targetAddress === void 0 ? [addrAuth] : collectSignatureNodes(credentials).filter( (node) => Address.fromScAddress(node.address()).toString() === targetAddress ); if (targets.length === 0) { throw new Error( `the authorization entry has no credential node for address ${targetAddress}` ); } targets.forEach((node) => node.signature(signatureScVal)); return clone; } function authorizeInvocation(params) { const { signer, validUntilLedgerSeq, invocation, networkPassphrase, publicKey = "", authV2 = false } = params; const kp = Keypair.random().rawPublicKey(); const nonce = new types.Int64(bytesToInt64(kp)); const pk = publicKey || (signer instanceof Keypair ? signer.publicKey() : null); if (!pk) { throw new Error(`authorizeInvocation requires publicKey parameter`); } const addressCredentials = new types.SorobanAddressCredentials({ address: new Address(pk).toScAddress(), nonce, signatureExpirationLedger: 0, // replaced signature: types.ScVal.scvVec([]) // replaced }); const entry = new types.SorobanAuthorizationEntry({ rootInvocation: invocation, credentials: authV2 ? types.SorobanCredentials.sorobanCredentialsAddressV2(addressCredentials) : types.SorobanCredentials.sorobanCredentialsAddress(addressCredentials) }); return authorizeEntry(entry, signer, validUntilLedgerSeq, networkPassphrase); } function buildAuthorizationEntryPreimage(entry, validUntilLedgerSeq, networkPassphrase) { const credentials = entry.credentials(); const addrAuth = getAddressCredentials(credentials); if (addrAuth === null) { throw new Error( `cannot build a signature payload for credential type ${credentials.switch().name}` ); } const networkId = hash(Buffer.from(networkPassphrase)); switch (credentials.switch().value) { // legacy address credentials are not address-bound case types.SorobanCredentialsType.sorobanCredentialsAddress().value: return types.HashIdPreimage.envelopeTypeSorobanAuthorization( new types.HashIdPreimageSorobanAuthorization({ networkId, nonce: addrAuth.nonce(), invocation: entry.rootInvocation(), signatureExpirationLedger: validUntilLedgerSeq }) ); // ADDRESS_V2 and ADDRESS_WITH_DELEGATES bind the address into the signed // payload via the WithAddress preimage (CAP-71) case types.SorobanCredentialsType.sorobanCredentialsAddressV2().value: case types.SorobanCredentialsType.sorobanCredentialsAddressWithDelegates().value: return types.HashIdPreimage.envelopeTypeSorobanAuthorizationWithAddress( new types.HashIdPreimageSorobanAuthorizationWithAddress({ networkId, nonce: addrAuth.nonce(), invocation: entry.rootInvocation(), address: addrAuth.address(), signatureExpirationLedger: validUntilLedgerSeq }) ); default: throw new Error( `unsupported credential type ${credentials.switch().name}` ); } } function buildWithDelegatesEntry(params) { const { entry, validUntilLedgerSeq, delegates, signature } = params; const credentials = entry.credentials(); const addrAuth = getAddressCredentials(credentials); if (addrAuth === null || credentials.switch().value === types.SorobanCredentialsType.sorobanCredentialsAddressWithDelegates().value) { throw new Error( `buildWithDelegatesEntry expects ADDRESS or ADDRESS_V2 credentials, got ${credentials.switch().name}` ); } return new types.SorobanAuthorizationEntry({ rootInvocation: entry.rootInvocation(), credentials: types.SorobanCredentials.sorobanCredentialsAddressWithDelegates( new types.SorobanAddressCredentialsWithDelegates({ addressCredentials: new types.SorobanAddressCredentials({ address: addrAuth.address(), nonce: addrAuth.nonce(), signatureExpirationLedger: validUntilLedgerSeq, signature: signature ?? types.ScVal.scvVoid() }), delegates: buildDelegateNodes(delegates) }) ) }); } function buildDelegateNodes(delegates) { const nodes = delegates.map( (delegate) => new types.SorobanDelegateSignature({ address: new Address(delegate.address).toScAddress(), signature: delegate.signature ?? types.ScVal.scvVoid(), nestedDelegates: buildDelegateNodes(delegate.nestedDelegates ?? []) }) ); nodes.sort( (a, b) => Buffer.compare(a.address().toXDR(), b.address().toXDR()) ); for (let i = 1; i < nodes.length; i++) { if (Buffer.compare( nodes[i - 1].address().toXDR(), nodes[i].address().toXDR() ) === 0) { throw new Error( `duplicate delegate address ${Address.fromScAddress( nodes[i].address() ).toString()}` ); } } return nodes; } function getAddressCredentials(credentials) { switch (credentials.switch().value) { case types.SorobanCredentialsType.sorobanCredentialsAddress().value: return credentials.address(); case types.SorobanCredentialsType.sorobanCredentialsAddressV2().value: return credentials.addressV2(); case types.SorobanCredentialsType.sorobanCredentialsAddressWithDelegates().value: return credentials.addressWithDelegates().addressCredentials(); default: return null; } } function collectSignatureNodes(credentials) { switch (credentials.switch().value) { case types.SorobanCredentialsType.sorobanCredentialsAddress().value: return [credentials.address()]; case types.SorobanCredentialsType.sorobanCredentialsAddressV2().value: return [credentials.addressV2()]; case types.SorobanCredentialsType.sorobanCredentialsAddressWithDelegates().value: { const withDelegates = credentials.addressWithDelegates(); const nodes = [withDelegates.addressCredentials()]; const walk = (delegates) => { delegates.forEach((delegate) => { nodes.push(delegate); walk(delegate.nestedDelegates()); }); }; walk(withDelegates.delegates()); return nodes; } default: return []; } } function inspectAuthEntry(entry) { const credentials = entry.credentials(); const addrAuth = getAddressCredentials(credentials); let credentialType; switch (credentials.switch().value) { case types.SorobanCredentialsType.sorobanCredentialsSourceAccount().value: credentialType = "sourceAccount"; break; case types.SorobanCredentialsType.sorobanCredentialsAddress().value: credentialType = "address"; break; case types.SorobanCredentialsType.sorobanCredentialsAddressV2().value: credentialType = "addressV2"; break; case types.SorobanCredentialsType.sorobanCredentialsAddressWithDelegates().value: credentialType = "addressWithDelegates"; break; default: throw new Error( `unsupported credential type ${credentials.switch().name}` ); } const signers = collectSignatureNodes(credentials).map( (node) => { const rawSignature = node.signature(); return { address: Address.fromScAddress(node.address()).toString(), signed: signaturePresent(rawSignature), signatures: parseEd25519Signatures(rawSignature), rawSignature }; } ); return { credentialType, address: addrAuth === null ? null : Address.fromScAddress(addrAuth.address()).toString(), nonce: addrAuth === null ? null : addrAuth.nonce().toBigInt(), signatureExpirationLedger: addrAuth === null ? null : addrAuth.signatureExpirationLedger(), signers, signed: signers.length > 0 && signers.every((signer) => signer.signed), invocation: entry.rootInvocation() }; } function checkAuthEntryReadiness(entry, currentLedgerSeq) { if (!Number.isInteger(currentLedgerSeq) || currentLedgerSeq < 0 || currentLedgerSeq > 4294967295) { throw new Error( `currentLedgerSeq must be a uint32 ledger sequence, got ${currentLedgerSeq}` ); } const info = inspectAuthEntry(entry); if (info.credentialType === "sourceAccount") { return { ready: true, expired: false, unsignedBy: [] }; } const expired = currentLedgerSeq >= (info.signatureExpirationLedger ?? 0); const unsignedBy = info.signers.filter((signer) => !signer.signed).map((signer) => signer.address); return { ready: !expired && unsignedBy.length === 0, expired, unsignedBy }; } function signaturePresent(signature) { switch (signature.switch().value) { case types.ScValType.scvVoid().value: return false; case types.ScValType.scvVec().value: return (signature.vec() ?? []).length > 0; default: return true; } } function parseEd25519Signatures(signature) { if (signature.switch().value !== types.ScValType.scvVec().value) { return null; } const parsed = []; for (const element of signature.vec() ?? []) { if (element.switch().value !== types.ScValType.scvMap().value) { return null; } let publicKey = null; let sig = null; for (const mapEntry of element.map() ?? []) { const key = mapEntry.key(); const val = mapEntry.val(); if (key.switch().value !== types.ScValType.scvSymbol().value || val.switch().value !== types.ScValType.scvBytes().value) { return null; } switch (key.sym().toString()) { case "public_key": publicKey = val.bytes(); break; case "signature": sig = val.bytes(); break; default: return null; } } if (publicKey === null || sig === null || publicKey.length !== 32 || sig.length !== 64) { return null; } parsed.push({ publicKey: StrKey.encodeEd25519PublicKey(publicKey), signature: sig }); } return parsed; } function bytesToInt64(bytes) { const buf = bytes.subarray(0, 8); if (buf.length < 8) { throw new Error( `need at least 8 bytes to convert to Int64, got ${bytes.length}` ); } const view = new DataView(buf.buffer, buf.byteOffset, 8); const value = view.getBigInt64(0, false); return value; } export { authorizeEntry, authorizeInvocation, buildAuthorizationEntryPreimage, buildWithDelegatesEntry, checkAuthEntryReadiness, getAddressCredentials, inspectAuthEntry }; //# sourceMappingURL=auth.js.map