@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
JavaScript
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