From 01e6baf2b782d26ccce1b92ba4452b57ed5ca344 Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Mon, 13 Oct 2025 18:22:41 -0300 Subject: [PATCH 01/90] feat: Implement the Moonlight transaction builder and operations conditions structure --- deno.json | 1 + src/conditions/index.ts | 52 +++ src/conditions/types.ts | 19 ++ src/core/config/index.ts | 21 ++ src/transaction-builder/index.ts | 464 +++++++++++++++++++++++++++ src/transaction-builder/types.ts | 28 ++ src/utils/auth/auth-entries.ts | 149 +++++++++ src/utils/auth/bundle-auth-entry.ts | 42 +++ src/utils/auth/deposit-auth-entry.ts | 53 +++ src/utils/common/index.ts | 39 +++ src/utils/types/stellar.types.ts | 1 + 11 files changed, 869 insertions(+) create mode 100644 src/conditions/index.ts create mode 100644 src/conditions/types.ts create mode 100644 src/core/config/index.ts create mode 100644 src/transaction-builder/index.ts create mode 100644 src/transaction-builder/types.ts create mode 100644 src/utils/auth/auth-entries.ts create mode 100644 src/utils/auth/bundle-auth-entry.ts create mode 100644 src/utils/auth/deposit-auth-entry.ts create mode 100644 src/utils/common/index.ts diff --git a/deno.json b/deno.json index 083d891..c8d7dae 100644 --- a/deno.json +++ b/deno.json @@ -26,6 +26,7 @@ "jsr:@noble/hashes/hkdf": "jsr:@noble/hashes/hkdf@latest", "jsr:@noble/curves/abstract/modular": "jsr:@noble/curves/abstract/modular@latest", "stellar-plus": "npm:stellar-plus@^0.14.1", + "stellar-sdk": "npm:@stellar/stellar-sdk@^14.0.0", "tslib": "npm:tslib@2.5.0", "buffer": "npm:buffer@6.0.3", diff --git a/src/conditions/index.ts b/src/conditions/index.ts new file mode 100644 index 0000000..abb6f12 --- /dev/null +++ b/src/conditions/index.ts @@ -0,0 +1,52 @@ +import { Condition } from "./types.ts"; +import { nativeToScVal, xdr } from "stellar-sdk"; +import { Buffer } from "buffer"; +import { CreateCondition, DepositCondition, WithdrawCondition } from "./types.ts"; + +const actionToSymbolStr = (action: Condition["action"]): string => { + if (action === "CREATE") return "Create"; + if (action === "DEPOSIT") return "Deposit"; + if (action === "WITHDRAW") return "Withdraw"; + throw new Error("Invalid action"); +}; + +export const conditionToXDR = (cond: Condition): xdr.ScVal => { + const actionXDR = xdr.ScVal.scvSymbol(actionToSymbolStr(cond.action)); + const addressXDR = + cond.action === "CREATE" + ? xdr.ScVal.scvBytes(Buffer.from(cond.utxo)) + : nativeToScVal(cond.publicKey, { type: "address" }); + const amountXDR = nativeToScVal(cond.amount, { type: "i128" }); + + const cXDR = xdr.ScVal.scvVec([actionXDR, addressXDR, amountXDR]); + + return cXDR; +}; + +export const createCondition = ( + utxo: Uint8Array, + amount: bigint +): CreateCondition => ({ + action: "CREATE", + utxo, + amount, +}); + +export const depositCondition = ( + publicKey: string, + amount: bigint +): DepositCondition => ({ + action: "DEPOSIT", + publicKey, + amount, +}); + +export const withdrawCondition = ( + publicKey: string, + amount: bigint +): WithdrawCondition => ({ + action: "WITHDRAW", + publicKey, + amount, +}); + \ No newline at end of file diff --git a/src/conditions/types.ts b/src/conditions/types.ts new file mode 100644 index 0000000..ffd9d1f --- /dev/null +++ b/src/conditions/types.ts @@ -0,0 +1,19 @@ +export type CreateCondition = { + action: "CREATE"; + utxo: Uint8Array; + amount: bigint; + }; + + export type DepositCondition = { + action: "DEPOSIT"; + publicKey: string; + amount: bigint; + }; + + export type WithdrawCondition = { + action: "WITHDRAW"; + publicKey: string; + amount: bigint; + }; + + export type Condition = CreateCondition | DepositCondition | WithdrawCondition; \ No newline at end of file diff --git a/src/core/config/index.ts b/src/core/config/index.ts new file mode 100644 index 0000000..93a7c92 --- /dev/null +++ b/src/core/config/index.ts @@ -0,0 +1,21 @@ +import { Server } from "@stellar/stellar-sdk/rpc"; +import { highlightText } from "../../utils/common" + +export function getRequiredEnv(key: string): string { + const value = Deno.env.get(key); + if (!value) { + console.error( + highlightText( + `Error: Environment variable ${key} is not set.\nCheck the 'Setup' section of the README.md file.`, + "red" + ) + ); + + throw new Error(`Required environment variable ${key} is not set. `); + } + return value; +} + +export const getRpc = () => { + return new Server(getRequiredEnv("STELLAR_RPC_URL"), { allowHttp: true }); +}; \ No newline at end of file diff --git a/src/transaction-builder/index.ts b/src/transaction-builder/index.ts new file mode 100644 index 0000000..583c9e8 --- /dev/null +++ b/src/transaction-builder/index.ts @@ -0,0 +1,464 @@ +import { + Asset, + authorizeEntry, + hash, + Keypair, + StrKey, + xdr, + nativeToScVal, +} from "@stellar/stellar-sdk"; +import { CreateOperation, DepositOperation, SpendOperation, WithdrawOperation, UTXOPublicKey, Ed25519PublicKey } from "../transaction-builder/types.ts"; +import { StellarSmartContractId } from "../utils/types/stellar.types.ts"; +import { Condition } from "../conditions/types.ts"; +import { sha256Buffer } from "../utils/hash/sha256Buffer.ts"; +import { generateNonce } from "../utils/common/index.ts"; +import { generateDepositAuthEntry } from "../utils/auth/deposit-auth-entry.ts"; +import { generateBundleAuthEntry } from "../utils/auth/bundle-auth-entry.ts"; +import { conditionToXDR } from "../conditions/index.ts"; +import { MoonlightOperation } from "../transaction-builder/types.ts"; + +export const createOpToXDR = (op: CreateOperation): xdr.ScVal => { + return xdr.ScVal.scvVec([ + xdr.ScVal.scvBytes(Buffer.from(op.utxo as Uint8Array)), + nativeToScVal(op.amount, { type: "i128" }), + ]); +}; + +export const depositOpToXDR = (op: DepositOperation): xdr.ScVal => { + return xdr.ScVal.scvVec([ + nativeToScVal(op.pubKey, { type: "address" }), + nativeToScVal(op.amount, { type: "i128" }), + op.conditions.length === 0 + ? xdr.ScVal.scvVec(null) + : xdr.ScVal.scvVec(op.conditions.map((c) => conditionToXDR(c))), + ]); +}; + +export const withdrawOpToXDR = (op: WithdrawOperation): xdr.ScVal => { + return xdr.ScVal.scvVec([ + nativeToScVal(op.pubKey, { type: "address" }), + nativeToScVal(op.amount, { type: "i128" }), + op.conditions.length === 0 + ? xdr.ScVal.scvVec(null) + : xdr.ScVal.scvVec(op.conditions.map((c) => conditionToXDR(c))), + ]); +}; + +export const spendOpToXDR = (op: SpendOperation): xdr.ScVal => { + return xdr.ScVal.scvVec([ + xdr.ScVal.scvBytes(Buffer.from(op.utxo as Uint8Array)), + op.conditions.length === 0 + ? xdr.ScVal.scvVec(null) + : xdr.ScVal.scvVec(op.conditions.map((c) => conditionToXDR(c))), + ]); +}; + +export class MoonlightTransactionBuilder { + private create: CreateOperation[] = []; + private spend: SpendOperation[] = []; + private deposit: DepositOperation[] = []; + private withdraw: WithdrawOperation[] = []; + private channelId: StellarSmartContractId; + private authId: StellarSmartContractId; + private asset: Asset; + private network: string; + private innerSignatures: Map< + Uint8Array, + { sig: Buffer; exp: number } + > = new Map(); + private providerInnerSignatures: Map< + Ed25519PublicKey, + { sig: Buffer; exp: number; nonce: string } + > = new Map(); + private extSignatures: Map = + new Map(); + + constructor({ + channelId, + authId, + asset, + network, + }: { + channelId: StellarSmartContractId; + authId: StellarSmartContractId; + asset: Asset; + network: string; + }) { + this.channelId = channelId; + this.authId = authId; + this.asset = asset; + this.network = network; + } + + addCreate(utxo: UTXOPublicKey, amount: bigint) { + if (this.create.find((c) => Buffer.from(c.utxo).equals(Buffer.from(utxo)))) + throw new Error("Create operation for this UTXO already exists"); + + if (amount <= 0n) + throw new Error("Create operation amount must be positive"); + + this.create.push({ utxo, amount }); + return this; + } + + addSpend(utxo: UTXOPublicKey, conditions: Condition[]) { + if (this.spend.find((s) => Buffer.from(s.utxo).equals(Buffer.from(utxo)))) + throw new Error("Spend operation for this UTXO already exists"); + + this.spend.push({ utxo, conditions }); + return this; + } + + addDeposit( + pubKey: Ed25519PublicKey, + amount: bigint, + conditions: Condition[] + ) { + if (this.deposit.find((d) => d.pubKey === pubKey)) + throw new Error("Deposit operation for this public key already exists"); + + if (amount <= 0n) + throw new Error("Deposit operation amount must be positive"); + + this.deposit.push({ pubKey, amount, conditions }); + return this; + } + + addWithdraw( + pubKey: Ed25519PublicKey, + amount: bigint, + conditions: Condition[] + ) { + if (this.withdraw.find((d) => d.pubKey === pubKey)) + throw new Error("Withdraw operation for this public key already exists"); + + if (amount <= 0n) + throw new Error("Withdraw operation amount must be positive"); + + this.withdraw.push({ pubKey, amount, conditions }); + return this; + } + + addInnerSignature( + utxo: UTXOPublicKey, + signature: Buffer, + expirationLedger: number + ) { + if (!this.spend.find((s) => Buffer.from(s.utxo).equals(Buffer.from(utxo)))) + throw new Error("No spend operation for this UTXO"); + + this.innerSignatures.set(utxo, { sig: signature, exp: expirationLedger }); + return this; + } + + addProviderInnerSignature( + pubKey: Ed25519PublicKey, + signature: Buffer, + expirationLedger: number, + nonce: string + ) { + this.providerInnerSignatures.set(pubKey, { + sig: signature, + exp: expirationLedger, + nonce, + }); + return this; + } + + addExtSignedEntry( + pubKey: Ed25519PublicKey, + signedAuthEntry: xdr.SorobanAuthorizationEntry + ) { + if ( + !this.deposit.find((d) => d.pubKey === pubKey) && + !this.withdraw.find((d) => d.pubKey === pubKey) + ) + throw new Error("No deposit or withdraw operation for this public key"); + + this.extSignatures.set(pubKey, signedAuthEntry); + return this; + } + + getOperation(): MoonlightOperation { + return { + create: this.create, + spend: this.spend, + deposit: this.deposit, + withdraw: this.withdraw, + }; + } + + getDepositOperation( + depositor: Ed25519PublicKey + ): DepositOperation | undefined { + return this.deposit.find((d) => d.pubKey === depositor); + } + + getExtAuthEntry( + address: Ed25519PublicKey, + nonce: string, + signatureExpirationLedger: number + ): xdr.SorobanAuthorizationEntry { + const deposit = this.getDepositOperation(address); + if (!deposit) throw new Error("No deposit operation for this address"); + + return generateDepositAuthEntry({ + channelId: this.channelId, + assetId: this.asset.contractId(this.network), + depositor: address, + amount: deposit.amount, + conditions: [xdr.ScVal.scvVec(deposit.conditions.map(conditionToXDR))], + nonce, + signatureExpirationLedger, + }); + } + + getAuthRequirementArgs(): xdr.ScVal[] { + if (this.spend.length === 0) return []; + + const signers: xdr.ScMapEntry[] = []; + + const orderedSpend = this.spend.sort((a, b) => + Buffer.from(a.utxo).compare(Buffer.from(b.utxo)) + ); + + for (const spend of orderedSpend) { + signers.push( + new xdr.ScMapEntry({ + key: xdr.ScVal.scvVec([ + xdr.ScVal.scvSymbol("P256"), + xdr.ScVal.scvBytes(Buffer.from(spend.utxo as Uint8Array)), + ]), + val: xdr.ScVal.scvVec(spend.conditions.map(conditionToXDR)), + }) + ); + } + + return [xdr.ScVal.scvVec([xdr.ScVal.scvMap(signers)])]; + } + + getOperationAuthEntry( + nonce: string, + signatureExpirationLedger: number, + signed: boolean = false + ): xdr.SorobanAuthorizationEntry { + const reqArgs: xdr.ScVal[] = this.getAuthRequirementArgs(); + + return generateBundleAuthEntry({ + channelId: this.channelId, + authId: this.authId, + args: reqArgs, + nonce, + signatureExpirationLedger, + signaturesXdr: signed ? this.signaturesXDR() : undefined, + }); + } + + getSignedOperationAuthEntry(): xdr.SorobanAuthorizationEntry { + const providerSigners = Array.from(this.providerInnerSignatures.keys()); + + if (providerSigners.length === 0) + throw new Error("No Provider signatures added"); + + const { nonce, exp: signatureExpirationLedger } = + this.providerInnerSignatures.get(providerSigners[0])!; + + const reqArgs: xdr.ScVal[] = this.getAuthRequirementArgs(); + + return generateBundleAuthEntry({ + channelId: this.channelId, + authId: this.authId, + args: reqArgs, + nonce, + signatureExpirationLedger, + signaturesXdr: this.signaturesXDR(), + }); + } + + async getOperationAuthEntryHash( + nonce: string, + signatureExpirationLedger: number + ): Promise { + const networkId = hash(Buffer.from(this.network)); + + const rootInvocation = this.getOperationAuthEntry( + nonce, + signatureExpirationLedger + ).rootInvocation(); + + const bundleHashPreImageInner = new xdr.HashIdPreimageSorobanAuthorization({ + networkId: networkId, + nonce: xdr.Int64.fromString(nonce), + signatureExpirationLedger, + invocation: rootInvocation, + }); + + const bundleHashPreImage = + xdr.HashIdPreimage.envelopeTypeSorobanAuthorization( + bundleHashPreImageInner + ); + + const xdrPayload = bundleHashPreImage.toXDR(); + + // Get the XDR buffer and hash it + return Buffer.from(await sha256Buffer(xdrPayload)); + } + + signaturesXDR(): string { + const providerSigners = Array.from(this.providerInnerSignatures.keys()); + const spendSigners = Array.from(this.innerSignatures.keys()); + + const ortderedProviderSigners = providerSigners.sort((a, b) => + a.localeCompare(b) + ); + const orderedSpendSigners = spendSigners.sort((a, b) => + Buffer.from(a).compare(Buffer.from(b)) + ); + + if (ortderedProviderSigners.length === 0) { + throw new Error("No Provider signatures added"); + } + + // MAPs must always be ordered by key so here it is providers -> P256 and each one ordered by pk + const signatures = xdr.ScVal.scvVec([ + xdr.ScVal.scvMap([ + ...orderedSpendSigners.map((utxo) => { + const { sig, exp } = this.innerSignatures.get(utxo)!; + + return new xdr.ScMapEntry({ + key: xdr.ScVal.scvVec([ + xdr.ScVal.scvSymbol("P256"), + xdr.ScVal.scvBytes(Buffer.from(utxo)), + ]), + val: xdr.ScVal.scvVec([ + xdr.ScVal.scvVec([ + xdr.ScVal.scvSymbol("P256"), + xdr.ScVal.scvBytes(sig), + ]), + + xdr.ScVal.scvU32(exp), + ]), + }); + }), + ...ortderedProviderSigners.map((pk) => { + const { sig, exp } = this.providerInnerSignatures.get(pk)!; + + return new xdr.ScMapEntry({ + key: xdr.ScVal.scvVec([ + xdr.ScVal.scvSymbol("Provider"), + xdr.ScVal.scvBytes(StrKey.decodeEd25519PublicKey(pk)), + ]), + val: xdr.ScVal.scvVec([ + xdr.ScVal.scvVec([ + xdr.ScVal.scvSymbol("Ed25519"), + xdr.ScVal.scvBytes(sig), + ]), + + xdr.ScVal.scvU32(exp), + ]), + }); + }), + ]), + ]); + + return signatures.toXDR("base64"); + } + + async signWithProvider( + providerKeys: Keypair, + signatureExpirationLedger: number, + nonce?: string + ) { + if (!nonce) nonce = generateNonce(); + + const signedHash = providerKeys.sign( + await this.getOperationAuthEntryHash(nonce, signatureExpirationLedger) + ); + + this.addProviderInnerSignature( + providerKeys.publicKey() as Ed25519PublicKey, + signedHash, + signatureExpirationLedger, + nonce + ); + } + + async signWithSpendUtxo( + utxo: IUTXOKeypairBase, + signatureExpirationLedger: number + ) { + const conditions = this.spend.find((s) => + Buffer.from(s.utxo).equals(Buffer.from(utxo.publicKey)) + )?.conditions; + if (!conditions) throw new Error("No spend operation for this UTXO"); + + const signedHash = await utxo.signPayload( + await buildAuthPayloadHash({ + contractId: this.channelId, + conditions, + liveUntilLedger: signatureExpirationLedger, + }) + ); + + this.addInnerSignature( + utxo.publicKey, + Buffer.from(signedHash), + signatureExpirationLedger + ); + } + + async signExtWithEd25519( + keys: Keypair, + signatureExpirationLedger: number, + nonce?: string + ) { + if (!nonce) nonce = generateNonce(); + + const rawAuthEntry = this.getExtAuthEntry( + keys.publicKey() as Ed25519PublicKey, + nonce, + signatureExpirationLedger + ); + + const signedAuthEntry = await authorizeEntry( + rawAuthEntry, + keys, + signatureExpirationLedger, + this.network + ); + + this.addExtSignedEntry( + keys.publicKey() as Ed25519PublicKey, + signedAuthEntry + ); + } + + getSignedAuthEntries(): xdr.SorobanAuthorizationEntry[] { + const signedEntries = [ + ...Array.from(this.extSignatures.values()), + this.getSignedOperationAuthEntry(), + ]; + return signedEntries; + } + + buildXDR(): xdr.ScVal { + return xdr.ScVal.scvMap([ + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("create"), + val: xdr.ScVal.scvVec(this.create.map((op) => createOpToXDR(op))), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("deposit"), + val: xdr.ScVal.scvVec(this.deposit.map((op) => depositOpToXDR(op))), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("spend"), + val: xdr.ScVal.scvVec(this.spend.map((op) => spendOpToXDR(op))), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("withdraw"), + val: xdr.ScVal.scvVec(this.withdraw.map((op) => withdrawOpToXDR(op))), + }), + ]); + } +} \ No newline at end of file diff --git a/src/transaction-builder/types.ts b/src/transaction-builder/types.ts new file mode 100644 index 0000000..bb241c1 --- /dev/null +++ b/src/transaction-builder/types.ts @@ -0,0 +1,28 @@ +import { Condition } from "../conditions/types.ts"; + +export type MoonlightOperation = { + create: CreateOperation[]; + spend: SpendOperation[]; + deposit: DepositOperation[]; + withdraw: WithdrawOperation[]; +}; + +export type SpendOperation = { utxo: UTXOPublicKey; conditions: Condition[] }; + +export type DepositOperation = { + pubKey: Ed25519PublicKey; + amount: bigint; + conditions: Condition[]; +}; + +export type WithdrawOperation = { + pubKey: Ed25519PublicKey; + amount: bigint; + conditions: Condition[]; +}; + +export type CreateOperation = { utxo: UTXOPublicKey; amount: bigint }; + +export type UTXOPublicKey = Uint8Array; + +export type Ed25519PublicKey = `G${string}`; \ No newline at end of file diff --git a/src/utils/auth/auth-entries.ts b/src/utils/auth/auth-entries.ts new file mode 100644 index 0000000..81c9fc6 --- /dev/null +++ b/src/utils/auth/auth-entries.ts @@ -0,0 +1,149 @@ +import { Address, nativeToScVal, scValToNative, xdr } from "stellar-sdk"; + +// deno-lint-ignore-file no-explicit-any +export interface AuthEntryParams { + credentials: { + address: string; + nonce: string; + signatureExpirationLedger: number; + signature?: string; + }; + rootInvocation: InvocationParams; +} + +export interface InvocationParams { + function: { + contractAddress: string; + functionName: string; + args: FnArg[] | xdr.ScVal[]; + }; + subInvocations?: InvocationParams[]; +} + +export interface FnArg { + value: any; + type: string; +} + +const invocationToParams = ( + invocation: xdr.SorobanAuthorizedInvocation +): InvocationParams => { + return { + function: { + contractAddress: Address.fromScAddress( + invocation.function().contractFn().contractAddress() + ).toString(), + functionName: invocation + .function() + .contractFn() + .functionName() + .toString(), + args: invocation.function().contractFn().args().map(parseScVal), + }, + subInvocations: [ + ...invocation + .subInvocations() + .map((subInvocation) => invocationToParams(subInvocation)), + ], + }; +}; + +export const paramsToInvocation = ( + params: InvocationParams +): xdr.SorobanAuthorizedInvocation => { + let args; + + if (params.function.args.length > 0 && "type" in params.function.args[0]) { + args = (params.function.args as FnArg[]).map((arg) => { + return nativeToScVal(arg.value, { type: arg.type }); + }); + } else { + args = params.function.args as xdr.ScVal[]; + } + + return new xdr.SorobanAuthorizedInvocation({ + function: + xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn( + new xdr.InvokeContractArgs({ + contractAddress: Address.fromString( + params.function.contractAddress + ).toScAddress(), + functionName: params.function.functionName, + args: args, + }) + ), + subInvocations: params.subInvocations?.map(paramsToInvocation) || [], + }); +}; + +export const authEntryToParams = ( + entry: xdr.SorobanAuthorizationEntry +): AuthEntryParams => { + const credentials = entry.credentials(); + const rootInvocation = entry.rootInvocation(); + + const entryParams: AuthEntryParams = { + credentials: { + address: Address.fromScAddress( + credentials.address().address() + ).toString(), + nonce: credentials.address().nonce().toString(), + signatureExpirationLedger: credentials + .address() + .signatureExpirationLedger(), + signature: credentials.address().signature().toXDR("base64"), + }, + rootInvocation: invocationToParams(rootInvocation), + }; + + return entryParams; +}; + +const parseScVal = (value: xdr.ScVal): FnArg => { + const type = parseScValType(value.switch().name); + return { + value: + type === "bool" ? scValToNative(value) : String(scValToNative(value)), + type, + }; +}; + +const parseScValType = (rawType: string): string => { + switch (rawType) { + case "scvAddress": + return "address"; + case "scvI128": + return "i128"; + case "scvBool": + return "bool"; + + default: + return rawType; + } +}; + +export const paramsToAuthEntry = ( + param: AuthEntryParams +): xdr.SorobanAuthorizationEntry => { + const credParams = param.credentials; + + return new xdr.SorobanAuthorizationEntry({ + rootInvocation: paramsToInvocation(param.rootInvocation), + credentials: xdr.SorobanCredentials.sorobanCredentialsAddress( + new xdr.SorobanAddressCredentials({ + address: Address.fromString(credParams.address).toScAddress(), + nonce: new xdr.Int64(credParams.nonce), + signatureExpirationLedger: credParams.signatureExpirationLedger, + signature: !credParams.signature + ? xdr.ScVal.scvVoid() + : xdr.ScVal.fromXDR(credParams.signature!, "base64"), + }) + ), + }); +}; + +export const paramsToAuthEntries = ( + authEntryParams: AuthEntryParams[] +): xdr.SorobanAuthorizationEntry[] => { + return authEntryParams.map(paramsToAuthEntry); +}; diff --git a/src/utils/auth/bundle-auth-entry.ts b/src/utils/auth/bundle-auth-entry.ts new file mode 100644 index 0000000..c6154dc --- /dev/null +++ b/src/utils/auth/bundle-auth-entry.ts @@ -0,0 +1,42 @@ +import { xdr } from "stellar-sdk"; +import { InvocationParams, paramsToAuthEntry } from "./auth-entries.ts"; + + +export const generateBundleAuthEntry = ({ + channelId, + + authId, + args, + nonce, + signatureExpirationLedger, + signaturesXdr, +}: { + channelId: string; + + authId: string; + args: xdr.ScVal[]; + nonce: string; + signatureExpirationLedger: number; + signaturesXdr?: string; +}): xdr.SorobanAuthorizationEntry => { + const rootInvocationParams = { + function: { + contractAddress: channelId, + functionName: "transact", + args, + }, + subInvocations: [], + } as InvocationParams; + + const entry = paramsToAuthEntry({ + credentials: { + address: authId, + nonce, + signatureExpirationLedger, + signature: signaturesXdr, + }, + rootInvocation: rootInvocationParams, + }); + + return entry; +}; diff --git a/src/utils/auth/deposit-auth-entry.ts b/src/utils/auth/deposit-auth-entry.ts new file mode 100644 index 0000000..5294207 --- /dev/null +++ b/src/utils/auth/deposit-auth-entry.ts @@ -0,0 +1,53 @@ +import { xdr } from "@stellar/stellar-sdk"; +import { FnArg, InvocationParams, paramsToAuthEntry } from "./auth-entries.ts"; + +export const generateDepositAuthEntry = ({ + channelId, + assetId, + depositor, + amount, + conditions, + nonce, + signatureExpirationLedger, +}: { + channelId: string; + assetId: string; + depositor: string; + amount: bigint; + conditions: xdr.ScVal[]; + nonce: string; + signatureExpirationLedger: number; +}): xdr.SorobanAuthorizationEntry => { + const rootInvocationParams = { + function: { + contractAddress: channelId, + functionName: "transact", + args: conditions, + }, + subInvocations: [ + { + function: { + contractAddress: assetId, + functionName: "transfer", + args: [ + { value: depositor, type: "address" }, + { value: channelId, type: "address" }, + { value: amount, type: "i128" }, + ] as FnArg[], + }, + subInvocations: [], + } as InvocationParams, + ], + } as InvocationParams; + + const entry = paramsToAuthEntry({ + credentials: { + address: depositor, + nonce, + signatureExpirationLedger, + }, + rootInvocation: rootInvocationParams, + }); + + return entry; +}; diff --git a/src/utils/common/index.ts b/src/utils/common/index.ts new file mode 100644 index 0000000..dfc1f4e --- /dev/null +++ b/src/utils/common/index.ts @@ -0,0 +1,39 @@ +/** + * Generates a random nonce as a string representation of an int64. + * + * @returns A string representing a random int64 value between 0 and 2^63-1 + */ +export function generateNonce(): string { + // Generate random bytes and convert to bigint + const randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + // Convert bytes to bigint, ensuring it's positive + let randomBigInt = 0n; + for (let i = 0; i < 8; i++) { + randomBigInt = (randomBigInt << 8n) + BigInt(randomBytes[i]); + } + + // Ensure it's within int64 range (remove sign bit) + randomBigInt = randomBigInt & 0x7fffffffffffffffn; + + return randomBigInt.toString(); +} + +export function highlightText( + text: string, + color: "red" | "green" | "yellow" | "blue" | "cyan" | "magenta" = "cyan" +): string { + const colors: Record = { + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + cyan: "\x1b[36m", + }; + + const reset = "\x1b[0m"; + return `${colors[color]}${text}${reset}`; +} + \ No newline at end of file diff --git a/src/utils/types/stellar.types.ts b/src/utils/types/stellar.types.ts index 9319a89..01ba1ca 100644 --- a/src/utils/types/stellar.types.ts +++ b/src/utils/types/stellar.types.ts @@ -1,2 +1,3 @@ export type StellarSecretKey = `S${string}`; export type StellarSmartContractId = `C${string}`; +// TODO: Consider move this types to use direct from Colibri (SEP-23 strkey) From 199316d8b4336e98ef66a5f2b2142237ffe58906 Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Mon, 13 Oct 2025 19:26:11 -0300 Subject: [PATCH 02/90] chore: Implement unit tests for the Moonlight transaction builder add methods --- deno.json | 1 + src/transaction-builder/index.unit.test.ts | 323 +++++++++++++++++++++ 2 files changed, 324 insertions(+) create mode 100644 src/transaction-builder/index.unit.test.ts diff --git a/deno.json b/deno.json index c8d7dae..beaad03 100644 --- a/deno.json +++ b/deno.json @@ -21,6 +21,7 @@ "@noble/curves": "jsr:@noble/curves", "@noble/hashes": "jsr:@noble/hashes", "@noble/secp256k1": "jsr:@noble/secp256k1", + "@stellar/stellar-sdk": "npm:@stellar/stellar-sdk@^14.2.0", "jsr:@noble/curves/p256": "jsr:@noble/curves/p256@latest", "jsr:@noble/hashes/sha256": "jsr:@noble/hashes/sha256@latest", "jsr:@noble/hashes/hkdf": "jsr:@noble/hashes/hkdf@latest", diff --git a/src/transaction-builder/index.unit.test.ts b/src/transaction-builder/index.unit.test.ts new file mode 100644 index 0000000..30ed6fb --- /dev/null +++ b/src/transaction-builder/index.unit.test.ts @@ -0,0 +1,323 @@ +// deno-lint-ignore-file require-await +import { + assertEquals, + assertNotEquals, + assertRejects, + assertThrows, +} from "https://deno.land/std@0.220.1/assert/mod.ts"; +import { MoonlightTransactionBuilder, createOpToXDR, depositOpToXDR, withdrawOpToXDR, spendOpToXDR } from "./index.ts"; +import { Asset, Keypair, xdr } from "stellar-sdk"; +import { IUTXOKeypairBase } from "../core/utxo-keypair-base/types.ts"; +import { Condition } from "../conditions/types.ts"; +import { StellarSmartContractId } from "../utils/types/stellar.types.ts"; + +// Mock data for testing +const mockChannelId: StellarSmartContractId = "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; +const mockAuthId: StellarSmartContractId = "CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; +const mockNetwork = "testnet"; +const mockAsset = Asset.native(); + +// Mock UTXO data +const mockUTXO1 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); +const mockUTXO2 = new Uint8Array([9, 10, 11, 12, 13, 14, 15, 16]); + +// Mock Ed25519 public keys +const mockEd25519Key1 = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; +const mockEd25519Key2 = "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; + +// Mock conditions +const mockCreateCondition: Condition = { + action: "CREATE", + utxo: mockUTXO1, + amount: 1000n, +}; + +const mockDepositCondition: Condition = { + action: "DEPOSIT", + publicKey: mockEd25519Key1, + amount: 500n, +}; + +const mockWithdrawCondition: Condition = { + action: "WITHDRAW", + publicKey: mockEd25519Key2, + amount: 300n, +}; + +// Mock IUTXOKeypairBase +class MockUTXOKeypair implements IUTXOKeypairBase { + publicKey: Uint8Array; + privateKey: Uint8Array; + + constructor(publicKey: Uint8Array, privateKey: Uint8Array) { + this.publicKey = publicKey; + this.privateKey = privateKey; + } + + async signPayload(payload: Uint8Array): Promise { + // Mock signature - return a fixed 64-byte signature + return new Uint8Array(64).fill(0x42); + } +} + +// Mock functions +const mockSha256Buffer = async (data: Uint8Array): Promise => { + // Return a mock hash + return new ArrayBuffer(32); +}; + +const mockGenerateNonce = (): string => { + return "1234567890123456789"; +}; + +const mockBuildAuthPayloadHash = async (params: { + contractId: string; + conditions: Condition[]; + liveUntilLedger: number; +}): Promise => { + // Return a mock payload hash + return new Uint8Array(32).fill(0xAA); +}; + +const mockGenerateDepositAuthEntry = (params: any): xdr.SorobanAuthorizationEntry => { + // Return a mock auth entry - simplified to avoid complex XDR construction + return {} as xdr.SorobanAuthorizationEntry; +}; + +const mockGenerateBundleAuthEntry = (params: any): xdr.SorobanAuthorizationEntry => { + // Return a mock auth entry - simplified to avoid complex XDR construction + return {} as xdr.SorobanAuthorizationEntry; +}; + +// Helper function to create a test builder instance +function createTestBuilder(): MoonlightTransactionBuilder { + return new MoonlightTransactionBuilder({ + channelId: mockChannelId, + authId: mockAuthId, + asset: mockAsset, + network: mockNetwork, + }); +} + +// Helper function to create a test Keypair +function createTestKeypair(): Keypair { + return Keypair.random(); +} + +Deno.test("MoonlightTransactionBuilder - Basic Operations (Add Methods)", async (t) => { + await t.step("addCreate should add create operation with valid parameters", () => { + const builder = createTestBuilder(); + + const result = builder.addCreate(mockUTXO1, 1000n); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Should have added the create operation + const operations = builder.getOperation(); + assertEquals(operations.create.length, 1); + assertEquals(operations.create[0].utxo, mockUTXO1); + assertEquals(operations.create[0].amount, 1000n); + }); + + await t.step("addCreate should throw error when UTXO already exists", () => { + const builder = createTestBuilder(); + + builder.addCreate(mockUTXO1, 1000n); + + // Should throw error when adding same UTXO again + assertThrows( + () => builder.addCreate(mockUTXO1, 2000n), + Error, + "Create operation for this UTXO already exists" + ); + }); + + await t.step("addCreate should throw error when amount is zero or negative", () => { + const builder = createTestBuilder(); + + // Should throw error for zero amount + assertThrows( + () => builder.addCreate(mockUTXO1, 0n), + Error, + "Create operation amount must be positive" + ); + + // Should throw error for negative amount + assertThrows( + () => builder.addCreate(mockUTXO2, -100n), + Error, + "Create operation amount must be positive" + ); + }); + + await t.step("addSpend should add spend operation with valid parameters", () => { + const builder = createTestBuilder(); + const conditions = [mockCreateCondition, mockDepositCondition]; + + const result = builder.addSpend(mockUTXO1, conditions); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Should have added the spend operation + const operations = builder.getOperation(); + assertEquals(operations.spend.length, 1); + assertEquals(operations.spend[0].utxo, mockUTXO1); + assertEquals(operations.spend[0].conditions.length, 2); + assertEquals(operations.spend[0].conditions[0], mockCreateCondition); + assertEquals(operations.spend[0].conditions[1], mockDepositCondition); + }); + + await t.step("addSpend should throw error when UTXO already exists", () => { + const builder = createTestBuilder(); + const conditions = [mockCreateCondition]; + + builder.addSpend(mockUTXO1, conditions); + + // Should throw error when adding same UTXO again + assertThrows( + () => builder.addSpend(mockUTXO1, [mockWithdrawCondition]), + Error, + "Spend operation for this UTXO already exists" + ); + }); + + await t.step("addSpend should handle empty conditions array", () => { + const builder = createTestBuilder(); + + const result = builder.addSpend(mockUTXO1, []); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Should have added the spend operation with empty conditions + const operations = builder.getOperation(); + assertEquals(operations.spend.length, 1); + assertEquals(operations.spend[0].utxo, mockUTXO1); + assertEquals(operations.spend[0].conditions.length, 0); + }); + + await t.step("addDeposit should add deposit operation with valid parameters", () => { + const builder = createTestBuilder(); + const conditions = [mockDepositCondition]; + + const result = builder.addDeposit(mockEd25519Key1, 500n, conditions); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Should have added the deposit operation + const operations = builder.getOperation(); + assertEquals(operations.deposit.length, 1); + assertEquals(operations.deposit[0].pubKey, mockEd25519Key1); + assertEquals(operations.deposit[0].amount, 500n); + assertEquals(operations.deposit[0].conditions.length, 1); + assertEquals(operations.deposit[0].conditions[0], mockDepositCondition); + }); + + await t.step("addDeposit should throw error when public key already exists", () => { + const builder = createTestBuilder(); + const conditions = [mockDepositCondition]; + + builder.addDeposit(mockEd25519Key1, 500n, conditions); + + // Should throw error when adding same public key again + assertThrows( + () => builder.addDeposit(mockEd25519Key1, 1000n, []), + Error, + "Deposit operation for this public key already exists" + ); + }); + + await t.step("addDeposit should throw error when amount is zero or negative", () => { + const builder = createTestBuilder(); + const conditions = [mockDepositCondition]; + + // Should throw error for zero amount + assertThrows( + () => builder.addDeposit(mockEd25519Key1, 0n, conditions), + Error, + "Deposit operation amount must be positive" + ); + + // Should throw error for negative amount + assertThrows( + () => builder.addDeposit(mockEd25519Key2, -100n, conditions), + Error, + "Deposit operation amount must be positive" + ); + }); + + await t.step("addWithdraw should add withdraw operation with valid parameters", () => { + const builder = createTestBuilder(); + const conditions = [mockWithdrawCondition]; + + const result = builder.addWithdraw(mockEd25519Key1, 300n, conditions); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Should have added the withdraw operation + const operations = builder.getOperation(); + assertEquals(operations.withdraw.length, 1); + assertEquals(operations.withdraw[0].pubKey, mockEd25519Key1); + assertEquals(operations.withdraw[0].amount, 300n); + assertEquals(operations.withdraw[0].conditions.length, 1); + assertEquals(operations.withdraw[0].conditions[0], mockWithdrawCondition); + }); + + await t.step("addWithdraw should throw error when public key already exists", () => { + const builder = createTestBuilder(); + const conditions = [mockWithdrawCondition]; + + builder.addWithdraw(mockEd25519Key1, 300n, conditions); + + // Should throw error when adding same public key again + assertThrows( + () => builder.addWithdraw(mockEd25519Key1, 500n, []), + Error, + "Withdraw operation for this public key already exists" + ); + }); + + await t.step("addWithdraw should throw error when amount is zero or negative", () => { + const builder = createTestBuilder(); + const conditions = [mockWithdrawCondition]; + + // Should throw error for zero amount + assertThrows( + () => builder.addWithdraw(mockEd25519Key1, 0n, conditions), + Error, + "Withdraw operation amount must be positive" + ); + + // Should throw error for negative amount + assertThrows( + () => builder.addWithdraw(mockEd25519Key2, -100n, conditions), + Error, + "Withdraw operation amount must be positive" + ); + }); + + await t.step("should allow chaining multiple operations", () => { + const builder = createTestBuilder(); + + const result = builder + .addCreate(mockUTXO1, 1000n) + .addCreate(mockUTXO2, 2000n) + .addSpend(mockUTXO1, [mockCreateCondition]) + .addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]) + .addWithdraw(mockEd25519Key2, 300n, [mockWithdrawCondition]); + + // Should return builder instance + assertEquals(result, builder); + + // Should have all operations + const operations = builder.getOperation(); + assertEquals(operations.create.length, 2); + assertEquals(operations.spend.length, 1); + assertEquals(operations.deposit.length, 1); + assertEquals(operations.withdraw.length, 1); + }); +}); From 640005089009b47a0fdcc4914d125cb08dd1c9f9 Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Mon, 13 Oct 2025 19:40:13 -0300 Subject: [PATCH 03/90] chore: Implement unit tests for internal signatures methods from the Moonlight transaction builder --- src/transaction-builder/index.unit.test.ts | 158 +++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/src/transaction-builder/index.unit.test.ts b/src/transaction-builder/index.unit.test.ts index 30ed6fb..7904651 100644 --- a/src/transaction-builder/index.unit.test.ts +++ b/src/transaction-builder/index.unit.test.ts @@ -321,3 +321,161 @@ Deno.test("MoonlightTransactionBuilder - Basic Operations (Add Methods)", async assertEquals(operations.withdraw.length, 1); }); }); + +Deno.test("MoonlightTransactionBuilder - Internal Signatures", async (t) => { + await t.step("addInnerSignature should add signature for existing spend operation", () => { + const builder = createTestBuilder(); + const mockSignature = new Uint8Array(64).fill(0x42); + const expirationLedger = 1000; + + // First add a spend operation + builder.addSpend(mockUTXO1, [mockCreateCondition]); + + const result = builder.addInnerSignature(mockUTXO1, mockSignature, expirationLedger); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Verify signature was added (we can't directly access private properties, + // but we can test that the method doesn't throw and returns the builder) + assertEquals(result instanceof MoonlightTransactionBuilder, true); + }); + + await t.step("addInnerSignature should throw error when UTXO not found in spend operations", () => { + const builder = createTestBuilder(); + const mockSignature = new Uint8Array(64).fill(0x42); + const expirationLedger = 1000; + + // Don't add any spend operations + + // Should throw error when trying to add signature for non-existent UTXO + assertThrows( + () => builder.addInnerSignature(mockUTXO1, mockSignature, expirationLedger), + Error, + "No spend operation for this UTXO" + ); + }); + + await t.step("addProviderInnerSignature should add provider signature", () => { + const builder = createTestBuilder(); + const mockSignature = new Uint8Array(64).fill(0x43); + const expirationLedger = 1000; + const nonce = "123456789"; + + const result = builder.addProviderInnerSignature( + mockEd25519Key1, + mockSignature, + expirationLedger, + nonce + ); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Verify the method doesn't throw and returns the builder + assertEquals(result instanceof MoonlightTransactionBuilder, true); + }); + + await t.step("addExtSignedEntry should add external signature for existing deposit", () => { + const builder = createTestBuilder(); + const mockAuthEntry = {} as xdr.SorobanAuthorizationEntry; + + // First add a deposit operation + builder.addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]); + + const result = builder.addExtSignedEntry(mockEd25519Key1, mockAuthEntry); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Verify the method doesn't throw and returns the builder + assertEquals(result instanceof MoonlightTransactionBuilder, true); + }); + + await t.step("addExtSignedEntry should add external signature for existing withdraw", () => { + const builder = createTestBuilder(); + const mockAuthEntry = {} as xdr.SorobanAuthorizationEntry; + + // First add a withdraw operation + builder.addWithdraw(mockEd25519Key1, 300n, [mockWithdrawCondition]); + + const result = builder.addExtSignedEntry(mockEd25519Key1, mockAuthEntry); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Verify the method doesn't throw and returns the builder + assertEquals(result instanceof MoonlightTransactionBuilder, true); + }); + + await t.step("addExtSignedEntry should throw error when public key not found", () => { + const builder = createTestBuilder(); + const mockAuthEntry = {} as xdr.SorobanAuthorizationEntry; + + // Don't add any deposit or withdraw operations + + // Should throw error when trying to add signature for non-existent public key + assertThrows( + () => builder.addExtSignedEntry(mockEd25519Key1, mockAuthEntry), + Error, + "No deposit or withdraw operation for this public key" + ); + }); + + await t.step("should allow chaining signature operations", () => { + const builder = createTestBuilder(); + const mockSignature = new Uint8Array(64).fill(0x44); + const mockAuthEntry = {} as xdr.SorobanAuthorizationEntry; + + // Add operations first + builder.addSpend(mockUTXO1, [mockCreateCondition]); + builder.addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]); + + const result = builder + .addInnerSignature(mockUTXO1, mockSignature, 1000) + .addProviderInnerSignature(mockEd25519Key1, mockSignature, 1000, "nonce123") + .addExtSignedEntry(mockEd25519Key1, mockAuthEntry); + + // Should return builder instance + assertEquals(result, builder); + + // Verify the method doesn't throw and returns the builder + assertEquals(result instanceof MoonlightTransactionBuilder, true); + }); + + await t.step("should handle multiple provider signatures", () => { + const builder = createTestBuilder(); + const mockSignature1 = new Uint8Array(64).fill(0x45); + const mockSignature2 = new Uint8Array(64).fill(0x46); + + const result = builder + .addProviderInnerSignature(mockEd25519Key1, mockSignature1, 1000, "nonce1") + .addProviderInnerSignature(mockEd25519Key2, mockSignature2, 1000, "nonce2"); + + // Should return builder instance + assertEquals(result, builder); + + // Verify the method doesn't throw and returns the builder + assertEquals(result instanceof MoonlightTransactionBuilder, true); + }); + + await t.step("should handle multiple inner signatures for different UTXOs", () => { + const builder = createTestBuilder(); + const mockSignature1 = new Uint8Array(64).fill(0x47); + const mockSignature2 = new Uint8Array(64).fill(0x48); + + // Add spend operations for different UTXOs + builder.addSpend(mockUTXO1, [mockCreateCondition]); + builder.addSpend(mockUTXO2, [mockDepositCondition]); + + const result = builder + .addInnerSignature(mockUTXO1, mockSignature1, 1000) + .addInnerSignature(mockUTXO2, mockSignature2, 1000); + + // Should return builder instance + assertEquals(result, builder); + + // Verify the method doesn't throw and returns the builder + assertEquals(result instanceof MoonlightTransactionBuilder, true); + }); +}); From 2cdfd0625354c79bd16ba85dd0ed4a3c9a0cdf81 Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Mon, 13 Oct 2025 20:01:02 -0300 Subject: [PATCH 04/90] chore: Update the Stellar SDK imports --- deno.json | 1 - src/conditions/index.ts | 2 +- src/transaction-builder/index.unit.test.ts | 2 +- src/utils/auth/auth-entries.ts | 2 +- src/utils/auth/bundle-auth-entry.ts | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/deno.json b/deno.json index beaad03..1dad911 100644 --- a/deno.json +++ b/deno.json @@ -27,7 +27,6 @@ "jsr:@noble/hashes/hkdf": "jsr:@noble/hashes/hkdf@latest", "jsr:@noble/curves/abstract/modular": "jsr:@noble/curves/abstract/modular@latest", "stellar-plus": "npm:stellar-plus@^0.14.1", - "stellar-sdk": "npm:@stellar/stellar-sdk@^14.0.0", "tslib": "npm:tslib@2.5.0", "buffer": "npm:buffer@6.0.3", diff --git a/src/conditions/index.ts b/src/conditions/index.ts index abb6f12..7131b35 100644 --- a/src/conditions/index.ts +++ b/src/conditions/index.ts @@ -1,5 +1,5 @@ import { Condition } from "./types.ts"; -import { nativeToScVal, xdr } from "stellar-sdk"; +import { nativeToScVal, xdr } from "@stellar/stellar-sdk"; import { Buffer } from "buffer"; import { CreateCondition, DepositCondition, WithdrawCondition } from "./types.ts"; diff --git a/src/transaction-builder/index.unit.test.ts b/src/transaction-builder/index.unit.test.ts index 7904651..fd2db25 100644 --- a/src/transaction-builder/index.unit.test.ts +++ b/src/transaction-builder/index.unit.test.ts @@ -6,7 +6,7 @@ import { assertThrows, } from "https://deno.land/std@0.220.1/assert/mod.ts"; import { MoonlightTransactionBuilder, createOpToXDR, depositOpToXDR, withdrawOpToXDR, spendOpToXDR } from "./index.ts"; -import { Asset, Keypair, xdr } from "stellar-sdk"; +import { Asset, Keypair, xdr } from "@stellar/stellar-sdk"; import { IUTXOKeypairBase } from "../core/utxo-keypair-base/types.ts"; import { Condition } from "../conditions/types.ts"; import { StellarSmartContractId } from "../utils/types/stellar.types.ts"; diff --git a/src/utils/auth/auth-entries.ts b/src/utils/auth/auth-entries.ts index 81c9fc6..30c980d 100644 --- a/src/utils/auth/auth-entries.ts +++ b/src/utils/auth/auth-entries.ts @@ -1,4 +1,4 @@ -import { Address, nativeToScVal, scValToNative, xdr } from "stellar-sdk"; +import { Address, nativeToScVal, scValToNative, xdr } from "@stellar/stellar-sdk"; // deno-lint-ignore-file no-explicit-any export interface AuthEntryParams { diff --git a/src/utils/auth/bundle-auth-entry.ts b/src/utils/auth/bundle-auth-entry.ts index c6154dc..0224f42 100644 --- a/src/utils/auth/bundle-auth-entry.ts +++ b/src/utils/auth/bundle-auth-entry.ts @@ -1,4 +1,4 @@ -import { xdr } from "stellar-sdk"; +import { xdr } from "@stellar/stellar-sdk"; import { InvocationParams, paramsToAuthEntry } from "./auth-entries.ts"; From a512964d6e6029d5daddab47d1f7a710fb68aca7 Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Tue, 14 Oct 2025 11:38:23 -0300 Subject: [PATCH 05/90] chore: Add unit test for the get operation methods --- src/transaction-builder/index.unit.test.ts | 99 ++++++++++------------ 1 file changed, 45 insertions(+), 54 deletions(-) diff --git a/src/transaction-builder/index.unit.test.ts b/src/transaction-builder/index.unit.test.ts index fd2db25..a0098ce 100644 --- a/src/transaction-builder/index.unit.test.ts +++ b/src/transaction-builder/index.unit.test.ts @@ -1,13 +1,10 @@ // deno-lint-ignore-file require-await import { assertEquals, - assertNotEquals, - assertRejects, assertThrows, } from "https://deno.land/std@0.220.1/assert/mod.ts"; -import { MoonlightTransactionBuilder, createOpToXDR, depositOpToXDR, withdrawOpToXDR, spendOpToXDR } from "./index.ts"; +import { MoonlightTransactionBuilder } from "./index.ts"; import { Asset, Keypair, xdr } from "@stellar/stellar-sdk"; -import { IUTXOKeypairBase } from "../core/utxo-keypair-base/types.ts"; import { Condition } from "../conditions/types.ts"; import { StellarSmartContractId } from "../utils/types/stellar.types.ts"; @@ -44,51 +41,6 @@ const mockWithdrawCondition: Condition = { amount: 300n, }; -// Mock IUTXOKeypairBase -class MockUTXOKeypair implements IUTXOKeypairBase { - publicKey: Uint8Array; - privateKey: Uint8Array; - - constructor(publicKey: Uint8Array, privateKey: Uint8Array) { - this.publicKey = publicKey; - this.privateKey = privateKey; - } - - async signPayload(payload: Uint8Array): Promise { - // Mock signature - return a fixed 64-byte signature - return new Uint8Array(64).fill(0x42); - } -} - -// Mock functions -const mockSha256Buffer = async (data: Uint8Array): Promise => { - // Return a mock hash - return new ArrayBuffer(32); -}; - -const mockGenerateNonce = (): string => { - return "1234567890123456789"; -}; - -const mockBuildAuthPayloadHash = async (params: { - contractId: string; - conditions: Condition[]; - liveUntilLedger: number; -}): Promise => { - // Return a mock payload hash - return new Uint8Array(32).fill(0xAA); -}; - -const mockGenerateDepositAuthEntry = (params: any): xdr.SorobanAuthorizationEntry => { - // Return a mock auth entry - simplified to avoid complex XDR construction - return {} as xdr.SorobanAuthorizationEntry; -}; - -const mockGenerateBundleAuthEntry = (params: any): xdr.SorobanAuthorizationEntry => { - // Return a mock auth entry - simplified to avoid complex XDR construction - return {} as xdr.SorobanAuthorizationEntry; -}; - // Helper function to create a test builder instance function createTestBuilder(): MoonlightTransactionBuilder { return new MoonlightTransactionBuilder({ @@ -99,11 +51,6 @@ function createTestBuilder(): MoonlightTransactionBuilder { }); } -// Helper function to create a test Keypair -function createTestKeypair(): Keypair { - return Keypair.random(); -} - Deno.test("MoonlightTransactionBuilder - Basic Operations (Add Methods)", async (t) => { await t.step("addCreate should add create operation with valid parameters", () => { const builder = createTestBuilder(); @@ -479,3 +426,47 @@ Deno.test("MoonlightTransactionBuilder - Internal Signatures", async (t) => { assertEquals(result instanceof MoonlightTransactionBuilder, true); }); }); + +Deno.test("MoonlightTransactionBuilder - Query Methods", async (t) => { + await t.step("getOperation should return empty arrays when no operations added", () => { + const builder = createTestBuilder(); + + const op = builder.getOperation(); + assertEquals(op.create.length, 0); + assertEquals(op.spend.length, 0); + assertEquals(op.deposit.length, 0); + assertEquals(op.withdraw.length, 0); + }); + + await t.step("getOperation should reflect added operations", () => { + const builder = createTestBuilder(); + + builder + .addCreate(mockUTXO1, 1000n) + .addSpend(mockUTXO1, [mockCreateCondition]) + .addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]) + .addWithdraw(mockEd25519Key2, 300n, [mockWithdrawCondition]); + + const op = builder.getOperation(); + assertEquals(op.create.length, 1); + assertEquals(op.spend.length, 1); + assertEquals(op.deposit.length, 1); + assertEquals(op.withdraw.length, 1); + }); + + await t.step("getDepositOperation should return deposit when exists", () => { + const builder = createTestBuilder(); + builder.addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]); + + const dep = builder.getDepositOperation(mockEd25519Key1); + assertEquals(dep?.pubKey, mockEd25519Key1); + assertEquals(dep?.amount, 500n); + assertEquals(dep?.conditions.length, 1); + }); + + await t.step("getDepositOperation should return undefined when not found", () => { + const builder = createTestBuilder(); + const dep = builder.getDepositOperation(mockEd25519Key2); + assertEquals(dep, undefined); + }); +}); From 913f0ea9d7f743ab9653dfc0fa894e01aaa8e3db Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Tue, 14 Oct 2025 12:06:00 -0300 Subject: [PATCH 06/90] chore: Add unit test for the auth entry and args --- src/transaction-builder/index.unit.test.ts | 65 ++++++++++++++++++++-- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/src/transaction-builder/index.unit.test.ts b/src/transaction-builder/index.unit.test.ts index a0098ce..84afb30 100644 --- a/src/transaction-builder/index.unit.test.ts +++ b/src/transaction-builder/index.unit.test.ts @@ -4,13 +4,13 @@ import { assertThrows, } from "https://deno.land/std@0.220.1/assert/mod.ts"; import { MoonlightTransactionBuilder } from "./index.ts"; -import { Asset, Keypair, xdr } from "@stellar/stellar-sdk"; +import { Asset, Keypair, StrKey, xdr } from "@stellar/stellar-sdk"; import { Condition } from "../conditions/types.ts"; import { StellarSmartContractId } from "../utils/types/stellar.types.ts"; // Mock data for testing -const mockChannelId: StellarSmartContractId = "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; -const mockAuthId: StellarSmartContractId = "CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; +const mockChannelId: StellarSmartContractId = StrKey.encodeContract(new Uint8Array(32)) as StellarSmartContractId; +const mockAuthId: StellarSmartContractId = StrKey.encodeContract(new Uint8Array(32).fill(1)) as StellarSmartContractId; const mockNetwork = "testnet"; const mockAsset = Asset.native(); @@ -19,8 +19,8 @@ const mockUTXO1 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); const mockUTXO2 = new Uint8Array([9, 10, 11, 12, 13, 14, 15, 16]); // Mock Ed25519 public keys -const mockEd25519Key1 = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; -const mockEd25519Key2 = "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; +const mockEd25519Key1 = Keypair.random().publicKey(); +const mockEd25519Key2 = Keypair.random().publicKey(); // Mock conditions const mockCreateCondition: Condition = { @@ -470,3 +470,58 @@ Deno.test("MoonlightTransactionBuilder - Query Methods", async (t) => { assertEquals(dep, undefined); }); }); + +Deno.test("MoonlightTransactionBuilder - Authorization and Arguments", async (t) => { + await t.step("getExtAuthEntry should generate entry for existing deposit", () => { + const builder = createTestBuilder(); + builder.addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]); + + // Using deterministic values for validation + const nonce = "123"; + const exp = 456; + + const entry = builder.getExtAuthEntry(mockEd25519Key1, nonce, exp); + // We can't assert XDR internals without full mocks; ensure object exists + assertEquals(!!entry, true); + }); + + await t.step("getExtAuthEntry should throw when deposit is missing", () => { + const builder = createTestBuilder(); + const nonce = "123"; + const exp = 456; + + assertThrows( + () => builder.getExtAuthEntry(mockEd25519Key1, nonce, exp), + Error, + "No deposit operation for this address", + ); + }); + + await t.step("getAuthRequirementArgs should return empty when no spend", () => { + const builder = createTestBuilder(); + const args = builder.getAuthRequirementArgs(); + assertEquals(Array.isArray(args), true); + assertEquals(args.length, 0); + }); + + await t.step("getAuthRequirementArgs should include ordered spend signers", () => { + const builder = createTestBuilder(); + // Add spend with two UTXOs in reverse order to verify ordering + builder + .addSpend(mockUTXO2, [mockDepositCondition]) + .addSpend(mockUTXO1, [mockCreateCondition]); + + const args = builder.getAuthRequirementArgs(); + // Expect one vector with one map of signers + assertEquals(args.length, 1); + // We can't deserialize xdr.ScVal here; presence suffices for unit test + assertEquals(!!args[0], true); + }); + + await t.step("getOperationAuthEntry should generate entry (unsigned)", () => { + const builder = createTestBuilder(); + // No spend: args should be empty, but entry is still generated + const entry = builder.getOperationAuthEntry("999", 1234, false); + assertEquals(!!entry, true); + }); +}); From 90393cea1ffb3871da259e96f468d69459441150 Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Tue, 14 Oct 2025 14:21:13 -0300 Subject: [PATCH 07/90] chore: Add unit test for the hash and signature XDR methods --- src/transaction-builder/index.unit.test.ts | 94 ++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/src/transaction-builder/index.unit.test.ts b/src/transaction-builder/index.unit.test.ts index 84afb30..70f8d00 100644 --- a/src/transaction-builder/index.unit.test.ts +++ b/src/transaction-builder/index.unit.test.ts @@ -525,3 +525,97 @@ Deno.test("MoonlightTransactionBuilder - Authorization and Arguments", async (t) assertEquals(!!entry, true); }); }); + +Deno.test("MoonlightTransactionBuilder - Hash and Signature XDR", async (t) => { + await t.step("getOperationAuthEntryHash should return hash for given parameters", async () => { + const builder = createTestBuilder(); + const nonce = "123456789"; + const exp = 1000; + + const hash = await builder.getOperationAuthEntryHash(nonce, exp); + // Should return a 32-byte hash + assertEquals(hash.length, 32); + assertEquals(hash instanceof Uint8Array, true); + }); + + await t.step("getOperationAuthEntryHash should use network ID correctly", async () => { + const builder = createTestBuilder(); + const nonce = "123456789"; + const exp = 1000; + + const hash1 = await builder.getOperationAuthEntryHash(nonce, exp); + const hash2 = await builder.getOperationAuthEntryHash(nonce, exp); + // Same parameters should produce same hash + assertEquals(hash1, hash2); + }); + + await t.step("getOperationAuthEntryHash should handle different nonce values", async () => { + const builder = createTestBuilder(); + const exp = 1000; + + const hash1 = await builder.getOperationAuthEntryHash("123456789", exp); + const hash2 = await builder.getOperationAuthEntryHash("987654321", exp); + // Different nonces should produce different hashes + assertEquals(hash1.length, 32); + assertEquals(hash2.length, 32); + // Hashes should be different + assertEquals(hash1.every((byte, i) => byte === hash2[i]), false); + }); + + await t.step("signaturesXDR should throw error when no provider signatures", () => { + const builder = createTestBuilder(); + // Add spend operation but no provider signature + builder.addSpend(mockUTXO1, [mockCreateCondition]); + + assertThrows( + () => builder.signaturesXDR(), + Error, + "No Provider signatures added", + ); + }); + + await t.step("signaturesXDR should return correct XDR format", () => { + const builder = createTestBuilder(); + const mockSignature = new Uint8Array(64).fill(0x42); + + // Add provider signature + builder.addProviderInnerSignature(mockEd25519Key1, mockSignature, 1000, "nonce123"); + + const xdrString = builder.signaturesXDR(); + // Should return a base64 XDR string + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + }); + + await t.step("signaturesXDR should order signatures correctly", () => { + const builder = createTestBuilder(); + const mockSignature1 = new Uint8Array(64).fill(0x42); + const mockSignature2 = new Uint8Array(64).fill(0x43); + + // Add provider signatures in reverse order + builder + .addProviderInnerSignature(mockEd25519Key2, mockSignature2, 1000, "nonce2") + .addProviderInnerSignature(mockEd25519Key1, mockSignature1, 1000, "nonce1"); + + const xdrString = builder.signaturesXDR(); + // Should return valid XDR string (ordering is internal, we just verify it works) + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + }); + + await t.step("signaturesXDR should handle both provider and spend signatures", () => { + const builder = createTestBuilder(); + const mockSignature = new Uint8Array(64).fill(0x44); + + // Add spend operation and signatures + builder + .addSpend(mockUTXO1, [mockCreateCondition]) + .addInnerSignature(mockUTXO1, mockSignature, 1000) + .addProviderInnerSignature(mockEd25519Key1, mockSignature, 1000, "nonce123"); + + const xdrString = builder.signaturesXDR(); + // Should return valid XDR string with both types + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + }); +}); From 5d86156709da1f7bfc231d788b20f7998a6da9eb Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Tue, 14 Oct 2025 16:04:37 -0300 Subject: [PATCH 08/90] chore: Add unit test for the high-level signing methods --- src/transaction-builder/index.ts | 2 + src/transaction-builder/index.unit.test.ts | 119 +++++++++++++++++++++ src/utils/auth/build-auth-payload.ts | 90 ++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 src/utils/auth/build-auth-payload.ts diff --git a/src/transaction-builder/index.ts b/src/transaction-builder/index.ts index 583c9e8..ad09401 100644 --- a/src/transaction-builder/index.ts +++ b/src/transaction-builder/index.ts @@ -16,6 +16,8 @@ import { generateDepositAuthEntry } from "../utils/auth/deposit-auth-entry.ts"; import { generateBundleAuthEntry } from "../utils/auth/bundle-auth-entry.ts"; import { conditionToXDR } from "../conditions/index.ts"; import { MoonlightOperation } from "../transaction-builder/types.ts"; +import { buildAuthPayloadHash } from "../utils/auth/build-auth-payload.ts"; +import { IUTXOKeypairBase } from "../core/utxo-keypair-base/types.ts"; export const createOpToXDR = (op: CreateOperation): xdr.ScVal => { return xdr.ScVal.scvVec([ diff --git a/src/transaction-builder/index.unit.test.ts b/src/transaction-builder/index.unit.test.ts index 70f8d00..0513c45 100644 --- a/src/transaction-builder/index.unit.test.ts +++ b/src/transaction-builder/index.unit.test.ts @@ -619,3 +619,122 @@ Deno.test("MoonlightTransactionBuilder - Hash and Signature XDR", async (t) => { assertEquals(xdrString.length > 0, true); }); }); + +Deno.test("MoonlightTransactionBuilder - High-Level Signing Methods", async (t) => { + await t.step("signWithProvider should sign with provided keypair", async () => { + const builder = createTestBuilder(); + const keypair = Keypair.random(); + const expirationLedger = 1000; + + await builder.signWithProvider(keypair, expirationLedger); + + // Verify that provider signature was added by checking signaturesXDR doesn't throw + const xdrString = builder.signaturesXDR(); + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + }); + + await t.step("signWithProvider should use provided nonce", async () => { + const builder = createTestBuilder(); + const keypair = Keypair.random(); + const expirationLedger = 1000; + const customNonce = "999888777"; + + await builder.signWithProvider(keypair, expirationLedger, customNonce); + + // Should not throw and should generate valid XDR + const xdrString = builder.signaturesXDR(); + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + }); + + await t.step("signWithSpendUtxo should throw error when UTXO not found", async () => { + const builder = createTestBuilder(); + const mockUtxo = { + publicKey: mockUTXO1, + signPayload: async (payload: Uint8Array) => new Uint8Array(64).fill(0x42) + }; + const expirationLedger = 1000; + + let errorThrown = false; + try { + await builder.signWithSpendUtxo(mockUtxo, expirationLedger); + } catch (error) { + errorThrown = true; + assertEquals(error.message, "No spend operation for this UTXO"); + } + assertEquals(errorThrown, true); + }); + + await t.step("signWithSpendUtxo should sign with UTXO keypair when found", async () => { + const builder = createTestBuilder(); + const mockUtxo = { + publicKey: mockUTXO1, + signPayload: async (payload: Uint8Array) => new Uint8Array(64).fill(0x42) + }; + const expirationLedger = 1000; + + // Add spend operation first + builder.addSpend(mockUTXO1, [mockCreateCondition]); + + await builder.signWithSpendUtxo(mockUtxo, expirationLedger); + + // Should not throw - signature was added + assertEquals(true, true); // Test passes if no exception + }); + + await t.step("signExtWithEd25519 should sign external auth entry", async () => { + const builder = createTestBuilder(); + const keypair = Keypair.random(); + const expirationLedger = 1000; + + // Add deposit operation first + builder.addDeposit(keypair.publicKey(), 500n, [mockDepositCondition]); + + await builder.signExtWithEd25519(keypair, expirationLedger); + + // Should not throw - external signature was added + assertEquals(true, true); // Test passes if no exception + }); + + await t.step("signExtWithEd25519 should use provided nonce", async () => { + const builder = createTestBuilder(); + const keypair = Keypair.random(); + const expirationLedger = 1000; + const customNonce = "555444333"; + + // Add deposit operation first + builder.addDeposit(keypair.publicKey(), 500n, [mockDepositCondition]); + + await builder.signExtWithEd25519(keypair, expirationLedger, customNonce); + + // Should not throw - custom nonce was used and signature added + assertEquals(true, true); // Test passes if no exception + }); + + await t.step("should handle complex signing workflow", async () => { + const builder = createTestBuilder(); + const providerKeypair = Keypair.random(); + const userKeypair = Keypair.random(); + const mockUtxo = { + publicKey: mockUTXO1, + signPayload: async (payload: Uint8Array) => new Uint8Array(64).fill(0x42) + }; + const expirationLedger = 1000; + + // Add operations + builder + .addSpend(mockUTXO1, [mockCreateCondition]) + .addDeposit(userKeypair.publicKey(), 500n, [mockDepositCondition]); + + // Sign with all methods (now that buildAuthPayloadHash is implemented) + await builder.signWithProvider(providerKeypair, expirationLedger); + await builder.signWithSpendUtxo(mockUtxo, expirationLedger); + await builder.signExtWithEd25519(userKeypair, expirationLedger); + + // Should generate valid XDR with all signatures + const xdrString = builder.signaturesXDR(); + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + }); +}); diff --git a/src/utils/auth/build-auth-payload.ts b/src/utils/auth/build-auth-payload.ts new file mode 100644 index 0000000..852f01d --- /dev/null +++ b/src/utils/auth/build-auth-payload.ts @@ -0,0 +1,90 @@ +import { Buffer } from "node:buffer"; +import { + Condition, + CreateCondition, + DepositCondition, + WithdrawCondition, +} from "../../conditions/types.ts"; + +export const buildAuthPayloadHash = ({ + contractId, + conditions, + liveUntilLedger, +}: { + contractId: string; + conditions: Condition[]; + liveUntilLedger: number; +}): Uint8Array => { + const encoder = new TextEncoder(); + + const encodedContractId = encoder.encode(contractId); + // const addr = Address.fromString(contractId); + // const addrXdr = addr.toScAddress().toXDR(); + const parts: Uint8Array[] = [encodedContractId]; + + const createConditions: CreateCondition[] = []; + const depositConditions: DepositCondition[] = []; + const withdrawConditions: WithdrawCondition[] = []; + + for (const condition of conditions) { + if (condition.action === "CREATE") { + createConditions.push(condition); + } else if (condition.action === "DEPOSIT") { + depositConditions.push(condition); + } else if (condition.action === "WITHDRAW") { + withdrawConditions.push(condition); + } + } + + // CREATE + // parts.push(encoder.encode("CREATE")); + for (const createCond of createConditions) { + parts.push(new Uint8Array(createCond.utxo)); + const amountBytes = bigintToLE(createCond.amount, 16); + parts.push(amountBytes); + } + // DEPOSIT + // parts.push(encoder.encode("DEPOSIT")); + for (const depositCond of depositConditions) { + // const addrXdr = Address.fromString(depositCond.publicKey) + // .toScAddress() + // .toXDR(); + parts.push(encoder.encode(depositCond.publicKey)); + parts.push(bigintToLE(depositCond.amount, 16)); + } + // WITHDRAW + // parts.push(encoder.encode("WITHDRAW")); + for (const withdrawCond of withdrawConditions) { + // const addrXdr = Address.fromString(withdrawCond.publicKey) + // .toScAddress() + // .toXDR(); + // parts.push(new Uint8Array(addrXdr)); + parts.push(encoder.encode(withdrawCond.publicKey)); + parts.push(bigintToLE(withdrawCond.amount, 16)); + } + + // parts.push(encoder.encode("INTEGRATE")); + // MOCK + + const encodedLiveUntil = bigintToLE(BigInt(liveUntilLedger), 4); + parts.push(encodedLiveUntil); + + // Concatenate all parts into one Uint8Array + const payloadBuffer = Buffer.concat(parts); + // const payload = new Uint8Array(payloadBuffer); + const payload = Buffer.concat(parts); + + // return sha256Buffer(payload); + return payload; +}; + +//convert bigint to little endian +export function bigintToLE(amount: bigint, byteLength: number): Uint8Array { + const result = new Uint8Array(byteLength); + let temp = amount; + for (let i = 0; i < byteLength; i++) { + result[i] = Number(temp & BigInt(0xff)); + temp = temp >> BigInt(8); + } + return result; +} From fa32ba262c6558ddf339726be01acb742952abe7 Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Tue, 14 Oct 2025 17:22:44 -0300 Subject: [PATCH 09/90] chore: Add unit tests for the XDR serialization and the auth entries retrieve method --- src/transaction-builder/index.unit.test.ts | 121 +++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/src/transaction-builder/index.unit.test.ts b/src/transaction-builder/index.unit.test.ts index 0513c45..56a386b 100644 --- a/src/transaction-builder/index.unit.test.ts +++ b/src/transaction-builder/index.unit.test.ts @@ -738,3 +738,124 @@ Deno.test("MoonlightTransactionBuilder - High-Level Signing Methods", async (t) assertEquals(xdrString.length > 0, true); }); }); + +Deno.test("MoonlightTransactionBuilder - Final Methods", async (t) => { + await t.step("getSignedAuthEntries should return all signed entries", async () => { + const builder = createTestBuilder(); + const providerKeypair = Keypair.random(); + const userKeypair = Keypair.random(); + const expirationLedger = 1000; + + // Add operations and sign + builder.addDeposit(userKeypair.publicKey(), 500n, [mockDepositCondition]); + await builder.signWithProvider(providerKeypair, expirationLedger); + await builder.signExtWithEd25519(userKeypair, expirationLedger); + + const signedEntries = builder.getSignedAuthEntries(); + + // Should return an array of signed auth entries + assertEquals(Array.isArray(signedEntries), true); + assertEquals(signedEntries.length, 2); // External + operation entry + }); + + await t.step("getSignedAuthEntries should include external and operation entries", async () => { + const builder = createTestBuilder(); + const providerKeypair = Keypair.random(); + const userKeypair = Keypair.random(); + const expirationLedger = 1000; + + // Add operations and sign + builder.addDeposit(userKeypair.publicKey(), 500n, [mockDepositCondition]); + await builder.signWithProvider(providerKeypair, expirationLedger); + await builder.signExtWithEd25519(userKeypair, expirationLedger); + + const signedEntries = builder.getSignedAuthEntries(); + + // Should have both external and operation entries + assertEquals(signedEntries.length >= 2, true); + + // Each entry should be a valid SorobanAuthorizationEntry + for (const entry of signedEntries) { + assertEquals(!!entry, true); + } + }); + + await t.step("buildXDR should include all operation types", () => { + const builder = createTestBuilder(); + + // Add one of each operation type + builder + .addCreate(mockUTXO1, 1000n) + .addSpend(mockUTXO1, [mockCreateCondition]) + .addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]) + .addWithdraw(mockEd25519Key2, 300n, [mockWithdrawCondition]); + + const xdr = builder.buildXDR(); + + // Should return valid XDR structure + assertEquals(!!xdr, true); + }); + + await t.step("buildXDR should handle empty operations correctly", () => { + const builder = createTestBuilder(); + + // Don't add any operations + const xdr = builder.buildXDR(); + + // Should still return valid XDR structure with empty arrays + assertEquals(!!xdr, true); + }); + + await t.step("buildXDR should handle mixed operations", () => { + const builder = createTestBuilder(); + + // Add multiple operations of different types + builder + .addCreate(mockUTXO1, 1000n) + .addCreate(mockUTXO2, 2000n) + .addSpend(mockUTXO1, [mockCreateCondition]) + .addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]) + .addDeposit(mockEd25519Key2, 300n, [mockWithdrawCondition]) + .addWithdraw(mockEd25519Key1, 200n, [mockWithdrawCondition]); + + const xdr = builder.buildXDR(); + + // Should return valid XDR structure + assertEquals(!!xdr, true); + }); + + await t.step("should handle complete transaction workflow", async () => { + const builder = createTestBuilder(); + const providerKeypair = Keypair.random(); + const userKeypair = Keypair.random(); + const mockUtxo = { + publicKey: mockUTXO1, + signPayload: async (payload: Uint8Array) => new Uint8Array(64).fill(0x42) + }; + const expirationLedger = 1000; + + // Complete workflow: add operations, sign, and build XDR + builder + .addCreate(mockUTXO1, 1000n) + .addSpend(mockUTXO1, [mockCreateCondition]) + .addDeposit(userKeypair.publicKey(), 500n, [mockDepositCondition]) + .addWithdraw(userKeypair.publicKey(), 200n, [mockWithdrawCondition]); + + // Sign with all methods + await builder.signWithProvider(providerKeypair, expirationLedger); + await builder.signWithSpendUtxo(mockUtxo, expirationLedger); + await builder.signExtWithEd25519(userKeypair, expirationLedger); + + // Get signed entries and build XDR + const signedEntries = builder.getSignedAuthEntries(); + const xdr = builder.buildXDR(); + const xdrString = builder.signaturesXDR(); + + // All should be valid + assertEquals(Array.isArray(signedEntries), true); + assertEquals(signedEntries.length >= 2, true); + assertEquals(!!xdr, true); + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + }); +}); From 91e7c44017877dc9f2fa6fdf9a802ff314aa480c Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Tue, 14 Oct 2025 17:35:33 -0300 Subject: [PATCH 10/90] chore: Add unit test for the deposit, spend, and withdraw operation to XDR methods --- src/transaction-builder/index.unit.test.ts | 154 ++++++++++++++++++++- 1 file changed, 153 insertions(+), 1 deletion(-) diff --git a/src/transaction-builder/index.unit.test.ts b/src/transaction-builder/index.unit.test.ts index 56a386b..6001534 100644 --- a/src/transaction-builder/index.unit.test.ts +++ b/src/transaction-builder/index.unit.test.ts @@ -3,7 +3,7 @@ import { assertEquals, assertThrows, } from "https://deno.land/std@0.220.1/assert/mod.ts"; -import { MoonlightTransactionBuilder } from "./index.ts"; +import { MoonlightTransactionBuilder, createOpToXDR, depositOpToXDR, withdrawOpToXDR, spendOpToXDR } from "./index.ts"; import { Asset, Keypair, StrKey, xdr } from "@stellar/stellar-sdk"; import { Condition } from "../conditions/types.ts"; import { StellarSmartContractId } from "../utils/types/stellar.types.ts"; @@ -859,3 +859,155 @@ Deno.test("MoonlightTransactionBuilder - Final Methods", async (t) => { assertEquals(xdrString.length > 0, true); }); }); + +Deno.test("Transaction Builder Utility Functions", async (t) => { + await t.step("createOpToXDR should convert create operation to XDR correctly", () => { + const createOp = { + utxo: mockUTXO1, + amount: 1000n + }; + + const xdr = createOpToXDR(createOp); + + // Should return a valid ScVal + assertEquals(!!xdr, true); + }); + + await t.step("depositOpToXDR should convert deposit operation to XDR correctly", () => { + const depositOp = { + pubKey: mockEd25519Key1, + amount: 500n, + conditions: [mockDepositCondition] + }; + + const xdr = depositOpToXDR(depositOp); + + // Should return a valid ScVal + assertEquals(!!xdr, true); + }); + + await t.step("depositOpToXDR should handle empty conditions", () => { + const depositOp = { + pubKey: mockEd25519Key1, + amount: 500n, + conditions: [] + }; + + const xdr = depositOpToXDR(depositOp); + + // Should return a valid ScVal even with empty conditions + assertEquals(!!xdr, true); + }); + + await t.step("withdrawOpToXDR should convert withdraw operation to XDR correctly", () => { + const withdrawOp = { + pubKey: mockEd25519Key1, + amount: 300n, + conditions: [mockWithdrawCondition] + }; + + const xdr = withdrawOpToXDR(withdrawOp); + + // Should return a valid ScVal + assertEquals(!!xdr, true); + }); + + await t.step("withdrawOpToXDR should handle empty conditions", () => { + const withdrawOp = { + pubKey: mockEd25519Key1, + amount: 300n, + conditions: [] + }; + + const xdr = withdrawOpToXDR(withdrawOp); + + // Should return a valid ScVal even with empty conditions + assertEquals(!!xdr, true); + }); + + await t.step("spendOpToXDR should convert spend operation to XDR correctly", () => { + const spendOp = { + utxo: mockUTXO1, + conditions: [mockCreateCondition, mockDepositCondition] + }; + + const xdr = spendOpToXDR(spendOp); + + // Should return a valid ScVal + assertEquals(!!xdr, true); + }); + + await t.step("spendOpToXDR should handle empty conditions", () => { + const spendOp = { + utxo: mockUTXO1, + conditions: [] + }; + + const xdr = spendOpToXDR(spendOp); + + // Should return a valid ScVal even with empty conditions + assertEquals(!!xdr, true); + }); + + await t.step("all utility functions should handle different amounts", () => { + // Test createOpToXDR with different amounts + const createOp1 = { utxo: mockUTXO1, amount: 1n }; + const createOp2 = { utxo: mockUTXO2, amount: 999999999n }; + + const xdr1 = createOpToXDR(createOp1); + const xdr2 = createOpToXDR(createOp2); + + assertEquals(!!xdr1, true); + assertEquals(!!xdr2, true); + + // Test depositOpToXDR with different amounts + const depositOp1 = { pubKey: mockEd25519Key1, amount: 1n, conditions: [] }; + const depositOp2 = { pubKey: mockEd25519Key2, amount: 999999999n, conditions: [] }; + + const xdr3 = depositOpToXDR(depositOp1); + const xdr4 = depositOpToXDR(depositOp2); + + assertEquals(!!xdr3, true); + assertEquals(!!xdr4, true); + + // Test withdrawOpToXDR with different amounts + const withdrawOp1 = { pubKey: mockEd25519Key1, amount: 1n, conditions: [] }; + const withdrawOp2 = { pubKey: mockEd25519Key2, amount: 999999999n, conditions: [] }; + + const xdr5 = withdrawOpToXDR(withdrawOp1); + const xdr6 = withdrawOpToXDR(withdrawOp2); + + assertEquals(!!xdr5, true); + assertEquals(!!xdr6, true); + }); + + await t.step("utility functions should handle multiple conditions", () => { + // Test with multiple conditions + const multipleConditions = [mockCreateCondition, mockDepositCondition, mockWithdrawCondition]; + + const depositOp = { + pubKey: mockEd25519Key1, + amount: 500n, + conditions: multipleConditions + }; + + const withdrawOp = { + pubKey: mockEd25519Key2, + amount: 300n, + conditions: multipleConditions + }; + + const spendOp = { + utxo: mockUTXO1, + conditions: multipleConditions + }; + + const depositXdr = depositOpToXDR(depositOp); + const withdrawXdr = withdrawOpToXDR(withdrawOp); + const spendXdr = spendOpToXDR(spendOp); + + assertEquals(!!depositXdr, true); + assertEquals(!!withdrawXdr, true); + assertEquals(!!spendXdr, true); + }); +}); From 777238bb9e56e5bc32cbb870079f6e987e613471 Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Tue, 14 Oct 2025 19:01:22 -0300 Subject: [PATCH 11/90] chore: Add unit tests for all the available operations and some edge cases --- src/transaction-builder/index.unit.test.ts | 294 +++++++++++++++++++++ 1 file changed, 294 insertions(+) diff --git a/src/transaction-builder/index.unit.test.ts b/src/transaction-builder/index.unit.test.ts index 6001534..608b1a0 100644 --- a/src/transaction-builder/index.unit.test.ts +++ b/src/transaction-builder/index.unit.test.ts @@ -1011,3 +1011,297 @@ Deno.test("Transaction Builder Utility Functions", async (t) => { assertEquals(!!spendXdr, true); }); }); + +Deno.test("MoonlightTransactionBuilder - Integration and Edge Cases", async (t) => { + await t.step("should build complete transaction with all operation types", async () => { + const builder = createTestBuilder(); + const providerKeypair = Keypair.random(); + const userKeypair1 = Keypair.random(); + const userKeypair2 = Keypair.random(); + const mockUtxo1 = { + publicKey: mockUTXO1, + signPayload: async (payload: Uint8Array) => new Uint8Array(64).fill(0x42) + }; + const mockUtxo2 = { + publicKey: mockUTXO2, + signPayload: async (payload: Uint8Array) => new Uint8Array(64).fill(0x43) + }; + const expirationLedger = 1000; + + // Add all types of operations + builder + .addCreate(mockUTXO1, 1000n) + .addCreate(mockUTXO2, 2000n) + .addSpend(mockUTXO1, [mockCreateCondition]) + .addSpend(mockUTXO2, [mockDepositCondition, mockWithdrawCondition]) + .addDeposit(userKeypair1.publicKey(), 500n, [mockDepositCondition]) + .addDeposit(userKeypair2.publicKey(), 300n, [mockWithdrawCondition]) + .addWithdraw(userKeypair1.publicKey(), 200n, [mockWithdrawCondition]) + .addWithdraw(userKeypair2.publicKey(), 100n, [mockCreateCondition]); + + // Sign with all methods + await builder.signWithProvider(providerKeypair, expirationLedger); + await builder.signWithSpendUtxo(mockUtxo1, expirationLedger); + await builder.signWithSpendUtxo(mockUtxo2, expirationLedger); + await builder.signExtWithEd25519(userKeypair1, expirationLedger); + await builder.signExtWithEd25519(userKeypair2, expirationLedger); + + // Verify all components work together + const operations = builder.getOperation(); + const signedEntries = builder.getSignedAuthEntries(); + const xdr = builder.buildXDR(); + const xdrString = builder.signaturesXDR(); + + // Validate operations + assertEquals(operations.create.length, 2); + assertEquals(operations.spend.length, 2); + assertEquals(operations.deposit.length, 2); + assertEquals(operations.withdraw.length, 2); + + // Validate signatures + assertEquals(Array.isArray(signedEntries), true); + assertEquals(signedEntries.length >= 3, true); // Provider + 2 external + + // Validate XDR + assertEquals(!!xdr, true); + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + }); + + await t.step("should handle complex transaction with multiple signatures", async () => { + const builder = createTestBuilder(); + const providerKeypair1 = Keypair.random(); + const providerKeypair2 = Keypair.random(); + const userKeypair1 = Keypair.random(); + const userKeypair2 = Keypair.random(); + const userKeypair3 = Keypair.random(); + const expirationLedger = 1000; + + // Add operations - each user keypair needs both deposit and withdraw operations + builder + .addCreate(mockUTXO1, 1000n) + .addSpend(mockUTXO1, [mockCreateCondition]) + .addDeposit(userKeypair1.publicKey(), 500n, [mockDepositCondition]) + .addDeposit(userKeypair2.publicKey(), 300n, [mockWithdrawCondition]) + .addDeposit(userKeypair3.publicKey(), 200n, [mockWithdrawCondition]) + .addWithdraw(userKeypair1.publicKey(), 200n, [mockWithdrawCondition]) + .addWithdraw(userKeypair2.publicKey(), 100n, [mockCreateCondition]) + .addWithdraw(userKeypair3.publicKey(), 150n, [mockDepositCondition]); + + // Add multiple provider signatures + await builder.signWithProvider(providerKeypair1, expirationLedger); + await builder.signWithProvider(providerKeypair2, expirationLedger); + + // Add multiple external signatures (each keypair has both deposit and withdraw) + await builder.signExtWithEd25519(userKeypair1, expirationLedger); + await builder.signExtWithEd25519(userKeypair2, expirationLedger); + await builder.signExtWithEd25519(userKeypair3, expirationLedger); + + // Verify multiple signatures are handled correctly + const signedEntries = builder.getSignedAuthEntries(); + const xdrString = builder.signaturesXDR(); + + assertEquals(Array.isArray(signedEntries), true); + assertEquals(signedEntries.length >= 4, true); // 2 providers + 3 external + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + }); + + await t.step("should validate transaction integrity", async () => { + const builder = createTestBuilder(); + const providerKeypair = Keypair.random(); + const userKeypair = Keypair.random(); + const mockUtxo = { + publicKey: mockUTXO1, + signPayload: async (payload: Uint8Array) => new Uint8Array(64).fill(0x42) + }; + const expirationLedger = 1000; + + // Build transaction + builder + .addCreate(mockUTXO1, 1000n) + .addSpend(mockUTXO1, [mockCreateCondition]) + .addDeposit(userKeypair.publicKey(), 500n, [mockDepositCondition]); + + await builder.signWithProvider(providerKeypair, expirationLedger); + await builder.signWithSpendUtxo(mockUtxo, expirationLedger); + await builder.signExtWithEd25519(userKeypair, expirationLedger); + + // Verify transaction integrity + const operations = builder.getOperation(); + const signedEntries = builder.getSignedAuthEntries(); + const xdr = builder.buildXDR(); + const xdrString = builder.signaturesXDR(); + + // All components should be consistent + assertEquals(operations.create.length, 1); + assertEquals(operations.spend.length, 1); + assertEquals(operations.deposit.length, 1); + assertEquals(operations.withdraw.length, 0); + + assertEquals(Array.isArray(signedEntries), true); + assertEquals(signedEntries.length >= 2, true); + assertEquals(!!xdr, true); + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + }); + + await t.step("should handle maximum number of operations", () => { + const builder = createTestBuilder(); + const maxOperations = 10; + + // Add maximum number of each operation type + for (let i = 0; i < maxOperations; i++) { + const utxo = new Uint8Array([i, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6, i + 7]); + const keypair = Keypair.random(); + + builder + .addCreate(utxo, BigInt(1000 + i)) + .addSpend(utxo, [mockCreateCondition]) + .addDeposit(keypair.publicKey(), BigInt(500 + i), [mockDepositCondition]) + .addWithdraw(keypair.publicKey(), BigInt(300 + i), [mockWithdrawCondition]); + } + + const operations = builder.getOperation(); + const xdr = builder.buildXDR(); + + assertEquals(operations.create.length, maxOperations); + assertEquals(operations.spend.length, maxOperations); + assertEquals(operations.deposit.length, maxOperations); + assertEquals(operations.withdraw.length, maxOperations); + assertEquals(!!xdr, true); + }); + + await t.step("should handle edge cases with zero amounts", () => { + const builder = createTestBuilder(); + + // These should throw errors for zero amounts + assertThrows( + () => builder.addCreate(mockUTXO1, 0n), + Error, + "Create operation amount must be positive" + ); + + assertThrows( + () => builder.addDeposit(mockEd25519Key1, 0n, []), + Error, + "Deposit operation amount must be positive" + ); + + assertThrows( + () => builder.addWithdraw(mockEd25519Key1, 0n, []), + Error, + "Withdraw operation amount must be positive" + ); + }); + + await t.step("should handle edge cases with negative amounts", () => { + const builder = createTestBuilder(); + + // These should throw errors for negative amounts + assertThrows( + () => builder.addCreate(mockUTXO1, -100n), + Error, + "Create operation amount must be positive" + ); + + assertThrows( + () => builder.addDeposit(mockEd25519Key1, -100n, []), + Error, + "Deposit operation amount must be positive" + ); + + assertThrows( + () => builder.addWithdraw(mockEd25519Key1, -100n, []), + Error, + "Withdraw operation amount must be positive" + ); + }); + + await t.step("should handle invalid input parameters", () => { + const builder = createTestBuilder(); + + // Test with empty UTXO array - should work but be a valid UTXO + const emptyUtxo = new Uint8Array(8).fill(0); + builder.addCreate(emptyUtxo, 1000n); + + // Try to add the same UTXO again - should throw error + assertThrows( + () => builder.addCreate(emptyUtxo, 2000n), + Error, + "Create operation for this UTXO already exists" + ); + + // Test with empty public key - should work but be a valid key + const emptyKey = "G" + "A".repeat(55); // Valid format but empty content + builder.addDeposit(emptyKey, 500n, []); + + // Try to add the same public key again - should throw error + assertThrows( + () => builder.addDeposit(emptyKey, 1000n, []), + Error, + "Deposit operation for this public key already exists" + ); + }); + + await t.step("should handle concurrent operations", async () => { + const builder = createTestBuilder(); + const providerKeypair = Keypair.random(); + const userKeypair = Keypair.random(); + const mockUtxo = { + publicKey: mockUTXO1, + signPayload: async (payload: Uint8Array) => new Uint8Array(64).fill(0x42) + }; + const expirationLedger = 1000; + + // Add operations + builder + .addCreate(mockUTXO1, 1000n) + .addSpend(mockUTXO1, [mockCreateCondition]) + .addDeposit(userKeypair.publicKey(), 500n, [mockDepositCondition]); + + // Sign concurrently (simulate concurrent access) + const signingPromises = [ + builder.signWithProvider(providerKeypair, expirationLedger), + builder.signWithSpendUtxo(mockUtxo, expirationLedger), + builder.signExtWithEd25519(userKeypair, expirationLedger) + ]; + + await Promise.all(signingPromises); + + // Verify all signatures were added + const signedEntries = builder.getSignedAuthEntries(); + const xdrString = builder.signaturesXDR(); + + assertEquals(Array.isArray(signedEntries), true); + assertEquals(signedEntries.length >= 2, true); + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + }); + + await t.step("should handle large transaction data", () => { + const builder = createTestBuilder(); + const largeAmount = 999999999999999999n; // Very large amount + const largeUtxo = new Uint8Array(64).fill(0xFF); // Large UTXO + const keypair = Keypair.random(); + + // Add operations with large data + builder + .addCreate(largeUtxo, largeAmount) + .addSpend(largeUtxo, [mockCreateCondition, mockDepositCondition, mockWithdrawCondition]) + .addDeposit(keypair.publicKey(), largeAmount, [mockDepositCondition]) + .addWithdraw(keypair.publicKey(), largeAmount, [mockWithdrawCondition]); + + const operations = builder.getOperation(); + const xdr = builder.buildXDR(); + + assertEquals(operations.create.length, 1); + assertEquals(operations.create[0].amount, largeAmount); + assertEquals(operations.spend.length, 1); + assertEquals(operations.deposit.length, 1); + assertEquals(operations.deposit[0].amount, largeAmount); + assertEquals(operations.withdraw.length, 1); + assertEquals(operations.withdraw[0].amount, largeAmount); + assertEquals(!!xdr, true); + }); +}); From 19a8305d42d721417d9ba6df1136089b20a80182 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 08:31:22 -0300 Subject: [PATCH 12/90] fix: Cast privateKeyBytes and payload to BufferSource in signPayload function for type safety --- src/utils/secp256r1/signPayload.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/secp256r1/signPayload.ts b/src/utils/secp256r1/signPayload.ts index 1b8ffcb..3000d55 100644 --- a/src/utils/secp256r1/signPayload.ts +++ b/src/utils/secp256r1/signPayload.ts @@ -7,7 +7,7 @@ export async function signPayload( // Import the private key from the raw PKCS8 format const privateKey = await crypto.subtle.importKey( "pkcs8", // Format of the private key - privateKeyBytes, // Raw private key bytes + privateKeyBytes as BufferSource, // Raw private key bytes { name: "ECDSA", namedCurve: "P-256" }, // Algorithm details false, // Non-extractable ["sign"] // Usage @@ -17,7 +17,7 @@ export async function signPayload( const signature = await crypto.subtle.sign( { name: "ECDSA", hash: { name: "SHA-256" } }, // Algorithm and hash privateKey, // Private key to sign with - payload // Data to sign + payload as BufferSource // Data to sign ); // Convert signature to Uint8Array From ee8e447eaf867408390d2da77200d2060300c961 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 08:31:48 -0300 Subject: [PATCH 13/90] fix: Cast data to BufferSource in sha256Buffer function for type safety --- src/utils/hash/sha256Buffer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/hash/sha256Buffer.ts b/src/utils/hash/sha256Buffer.ts index 973edef..e240aeb 100644 --- a/src/utils/hash/sha256Buffer.ts +++ b/src/utils/hash/sha256Buffer.ts @@ -1,4 +1,4 @@ export async function sha256Buffer(data: Uint8Array): Promise { // Compute the SHA-256 digest (returns an ArrayBuffer) - return await crypto.subtle.digest("SHA-256", data); + return await crypto.subtle.digest("SHA-256", data as BufferSource); } From 6c7d93c9ed928d4da9fe1ad0ea7cd5a021665488 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 08:32:28 -0300 Subject: [PATCH 14/90] =?UTF-8?q?refactor:=20Remove=20unused=20pool=20and?= =?UTF-8?q?=20pool-engine=20files=20for=20codebase=20cleanup=20?= =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Deleted `index.ts` from `src/pool/` as it was exporting an unused module. - Removed `index.ts`, `types.ts`, and `index.unit.test.ts` from `src/pool/pool-engine/` to eliminate unnecessary code and improve maintainability. --- index.ts | 276 ------------------------ src/pool/index.ts | 1 - src/pool/pool-engine/index.ts | 253 ---------------------- src/pool/pool-engine/index.unit.test.ts | 207 ------------------ src/pool/pool-engine/types.ts | 135 ------------ src/pool/types.ts | 1 - 6 files changed, 873 deletions(-) delete mode 100644 index.ts delete mode 100644 src/pool/index.ts delete mode 100644 src/pool/pool-engine/index.ts delete mode 100644 src/pool/pool-engine/index.unit.test.ts delete mode 100644 src/pool/pool-engine/types.ts delete mode 100644 src/pool/types.ts diff --git a/index.ts b/index.ts deleted file mode 100644 index d961597..0000000 --- a/index.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { StellarPlus } from "stellar-plus"; -import { - Bundle, - PermissionlessPool, -} from "./src/pool/permissionlessPoolClient.ts"; -import { TransactionInvocation } from "stellar-plus/lib/stellar-plus/types"; -import { Buffer } from "buffer"; -import { StellarPlusErrorObject as SPError } from "stellar-plus/lib/stellar-plus/error/types"; -import { hash } from "node:crypto"; -import { SPPAccount } from "./src/account/sppAccount.ts"; -import { - StellarNetwork, - StellarNetworkDerivatorFactory, -} from "./src/account/derivation/schemas/stellar.ts"; -import { UTXOPublicKey } from "./src/core/utxo-keypair-base/types.ts"; -import { ExecutorClient } from "./src/executor/client.ts"; -import { SelectionDirective } from "./mod.ts"; -const { TestNet } = StellarPlus.Network; -const XLM_CONTRACT_ID = - "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; -// console.log(a); - -const XLM = new StellarPlus.Asset.SACHandler({ - networkConfig: TestNet(), - code: "XLM", - contractParameters: { - contractId: XLM_CONTRACT_ID, - }, -}); -const pool = new PermissionlessPool({ - assetContractId: XLM_CONTRACT_ID, - networkConfig: TestNet(), -}); - -const admin = new StellarPlus.Account.DefaultAccountHandler({ - networkConfig: TestNet(), - secretKey: "SB6MNQ3CF7C2AYEUM6HRKSRPAPXX6NRXFJSCGIYR2DHSCIMFPTO5KLA7", -}); - -const txInvocation: TransactionInvocation = { - header: { - source: admin.getPublicKey(), - fee: "100000", - timeout: 30, - }, - signers: [admin], -}; - -const run = async () => { - // console.log("Creating admin: ", admin.getPublicKey()); - // await admin.initializeWithFriendbot(); - - console.log( - "Admin initialized with balance: ", - await XLM.classicHandler.balance(admin.getPublicKey()) - ); - - const derivatorFactory = StellarNetworkDerivatorFactory({ - network: StellarNetwork.Testnet, - smartContract: pool.getContractId(), - }); - - const account = new SPPAccount({ - networkConfig: TestNet(), - secretKey: "SB5UPGAHUHJ53GZNAMEQKLHPGIKDUY3Y37THVTRXBFQGKBFCOVAQD7EF", - derivatorFactory, - utxoBalances: (publicKeys: UTXOPublicKey[]) => { - return pool.balances({ - utxos: publicKeys.map((pk) => Buffer.from(pk)), - txInvocation, - }); - }, - }); - - console.log("Secret: ", account.getSecretKey()); - await account.deriveAndLoad(500); - - // ==========DEPOSIT =============== - - // const depSeqs = await account.selectFreeUtxos(3); - - // console.log("Depositing"); - // await pool.deposit({ - // amount: 50000n, - // from: admin.getPublicKey(), - // txInvocation, - // utxo: Buffer.from(account.getUtxo(depSeqs[0]).keypair.publicKey), - // }); - - // await account.updateAndReleaseUtxos(depSeqs); - // ==========TRANSFER================ - { - console.log("Reserved Sequences: ", account.getReservedSequences()); - - const receivers = 40; - const selectedSequences = await account.selectFreeUtxos(receivers); - - //divides 1200 into two random numbers - let totalAllocated = 0; - const parts = []; - - for (let i = 0; i < receivers; i++) { - const part = Math.floor( - Math.random() * - (Number(await account.getUnspentBalance()) / 10 / receivers) - ); - - totalAllocated += part; - parts.push(part); - } - - console.log("TOTAL ALLOC: ", totalAllocated); - - const { senders, change } = await account.getSenderSample( - BigInt(totalAllocated), - SelectionDirective.RANDOM - ); - - console.log( - "selected senders Sequences: ", - senders.map((sender) => sender.derivation.sequence) - ); - - console.log("Parts: ", parts); - - const bundle: Bundle = { - spend: [ - ...senders.map((sender) => Buffer.from(sender.keypair.publicKey)), - ], - create: parts.map((p, i) => [ - Buffer.from(account.getUtxo(selectedSequences[i]).keypair.publicKey), - BigInt(p), - ]), - signatures: [], - }; - - console.log("checking for change ", change); - if (change) { - // bundle.create.push([ - // Buffer.from(account.getUtxo(selectedSequences[2]).keypair.publicKey), - // BigInt(change), - // ]); - } - - const payload = pool.buildBundlePayload(bundle); - - bundle.signatures = [ - ...(await account.signUtxos( - payload, - senders.map((sender) => sender.derivation.sequence) - )), - ]; - - console.log("Sending bundle"); - - const executor = new ExecutorClient({ - apiUrl: "http://localhost:8000", - }); - - const res = await executor.delegatedTransfer([bundle]); - console.log("Response: ", res); - - // const unbufferizedBundle = { - // spend: bundle.spend.map((spend) => new Uint8Array(spend)), // ✅ Keep Uint8Array - // create: bundle.create.map(([pk, amount]) => [ - // new Uint8Array(pk), // ✅ Keep Uint8Array - // amount.toString(), // ✅ Convert BigInt to string - // ]), - // signatures: bundle.signatures.map( - // (signature) => new Uint8Array(signature) - // ), // ✅ Keep Uint8Array - // }; - - // const apiUrl = "http://localhost:8000/execute"; - - // const execPayload = { - // bundles: [unbufferizedBundle], - // }; - - // // ✅ Custom JSON.stringify to handle BigInt - // const jsonPayload = JSON.stringify(execPayload, (_, value) => { - // if (value instanceof Uint8Array) { - // return Array.from(value); // ✅ Convert Uint8Array to plain array - // } - // if (typeof value === "bigint") { - // return value.toString(); // ✅ Convert BigInt to string - // } - // return value; - // }); - - // const response = await fetch(apiUrl, { - // method: "POST", - // headers: { "Content-Type": "application/json" }, - // body: jsonPayload, - // }); - - // const data = await response.json(); - // console.log("Response:", data); - - // await pool.transfer({ - // bundles: [bundle], - // txInvocation, - // }); - - // console.log("Updating"); - // await account.updateAndReleaseUtxos([ - // ...senders.map((sender) => sender.derivation.sequence), - // ...selectedSequences, - // ]); - - // console.log("local balance: ", account.getUnspentBalance()); - // console.log("UTXOs: "); - // account.getUTXOs().forEach((utxo) => { - // console.log( - // "UTXO: ", - // utxo.sequence, - // " status: ", - // utxo.status, - // " balance: ", - // utxo.balance - // ); - // }); - } - // ==============DEPOSIT================ - // { - // const selectedSequences = await account.selectFreeUtxos(3); - - // console.log("Depositing 1000"); - // await pool.deposit({ - // from: admin.getPublicKey(), - // amount: 10000n, - // utxo: Buffer.from( - // account.getUtxo(selectedSequences[0]).keypair.publicKey - // ), - // txInvocation, - // }); - - // console.log("Depositing 500"); - // await pool.deposit({ - // from: admin.getPublicKey(), - // amount: 5000n, - // utxo: Buffer.from( - // account.getUtxo(selectedSequences[1]).keypair.publicKey - // ), - // txInvocation, - // }); - - // console.log("Depositing 125"); - // await pool.deposit({ - // from: admin.getPublicKey(), - // amount: 7000n, - // utxo: Buffer.from( - // account.getUtxo(selectedSequences[2]).keypair.publicKey - // ), - // txInvocation, - // }); - - // console.log("Deposited!"); - // console.log("local balance: ", account.getUnspentBalance()); - // console.log("Unspent UTXOs: ", account.getUnspentBalances()); - - // console.log("updating"); - - // await account.loadUTXOs(selectedSequences); - // console.log("local balance: ", account.getUnspentBalance()); - // console.log("Unspent UTXOs: ", account.getUnspentBalances()); - // } -}; - -run().catch((e) => { - console.log(e); - - // console.log((e as SPError).meta); - - // console.log("XDR: ", (e as SPError).?.); -}); diff --git a/src/pool/index.ts b/src/pool/index.ts deleted file mode 100644 index 5e56fde..0000000 --- a/src/pool/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./pool-engine/index.ts"; diff --git a/src/pool/pool-engine/index.ts b/src/pool/pool-engine/index.ts deleted file mode 100644 index 799f92e..0000000 --- a/src/pool/pool-engine/index.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { StellarPlus } from "stellar-plus"; -import type { - BaseInvocation, - ContractEngineConstructorArgs, - SorobanInvokeArgs, - SorobanSimulateArgs, -} from "stellar-plus/lib/stellar-plus/core/contract-engine/types"; -import { - type PoolEngineConstructorArgs, - type ContractConstructorArgs, - type ReadMapping, - type WriteMapping, - type Bundle, - type BundlePayloadAction, - SimplePayloadAction, -} from "./types.ts"; - -import type { ReadMethods, WriteMethods } from "./types.ts"; -import type { SorobanTransactionPipelineOutput } from "stellar-plus/lib/stellar-plus/core/pipelines/soroban-transaction/types"; - -import type { SorobanTransactionPipelineInput as _SorobanTransactionPipelineInput } from "stellar-plus/lib/stellar-plus/core/pipelines/soroban-transaction/types"; -import { Buffer } from "buffer"; -import { bigintToLE } from "../../utils/conversion/bigintToLE.ts"; -import { - StellarDerivator, - type StellarNetworkId, -} from "../../derivation/index.ts"; -import type { StellarSmartContractId } from "../../utils/types/stellar.types.ts"; - -const ContractEngine = StellarPlus.Core.ContractEngine; - -export class PoolEngine extends ContractEngine { - public assetContractId: string; - public derivator: StellarDerivator; - - private _networkId: StellarNetworkId; - - constructor({ - networkConfig, - wasm, - assetContractId, - poolContractId, - }: PoolEngineConstructorArgs) { - super({ - networkConfig, - contractParameters: { - wasm, - contractId: poolContractId, - }, - } as ContractEngineConstructorArgs); - - this._networkId = networkConfig.networkPassphrase as StellarNetworkId; - this.assetContractId = assetContractId; - - const derivator = new StellarDerivator(); - this.derivator = derivator; - } - - /** - * Creates a new instance of the PoolEngine class and loads the WASM specification. - * @param {PoolEngineConstructorArgs} args - The constructor arguments. - * @returns {Promise} A promise that resolves to a new instance of the PoolEngine class. - */ - public static async create( - args: PoolEngineConstructorArgs - ): Promise { - const pool = new PoolEngine(args); - await pool.loadSpecFromWasm(); - return pool; - } - - /** - * Sets the context for the contract engine. - * This method is used to set the network and contract ID for the contract engine. - */ - public setContext() { - const contractId = this.getContractId(); - const networkId = this._networkId; - this.derivator.withNetworkAndContract( - networkId, - contractId as StellarSmartContractId - ); - } - - /** - * Deploys a new contract instance and initializes it with the provided arguments. - * The asset contract ID is set to the assetContractId of the PoolEngine instance. - * - * This method overrides the deploy method of the ContractEngine class and automatically - * sets the derivation context for the contract. - * - * @param {BaseInvocation} args - The transaction invocation arguments. - * @param {string} args.contractArgs.admin - The admin address. - * */ - public override async deploy( - args: BaseInvocation & { - contractArgs: { - admin: string; - }; - } - ): Promise { - const result = await super.deploy({ - ...args, - contractArgs: { - asset: this.assetContractId, - admin: args.contractArgs.admin, // Use the admin value from contractArgs instead of header.source - } as ContractConstructorArgs, - }); - - this.setContext(); - return result; - } - - /** - * Reads the contract state using the specified method and arguments. - * - * @param {BaseInvocation} args - The transaction invocation arguments. - * @param { _SorobanTransactionPipelineInput['options']} args.options - The options for the transaction pipeline. - * @param {M} args.method - The read method to call. - * @param {ReadMapping[M]["input"]} args.methodArgs - The arguments for the read method. - * @returns {Promise} A promise that resolves to the output of the read method. - * */ - - public async read( - args: BaseInvocation & { method: M; methodArgs: ReadMapping[M]["input"] } - ): Promise { - return (await this.readFromContract( - args as SorobanSimulateArgs - )) as Promise; - } - - /** - * Writes to the contract state using the specified method and arguments. - * - * @param {BaseInvocation} args - The transaction invocation arguments. - * @param { SorobanGetTransactionPipelineInput['options']} args.options - The options for the transaction pipeline. - * @param {M} args.method - The write method to call. - * @param {WriteMapping[M]["input"]} args.methodArgs - The arguments for the write method. - * @returns {Promise} A promise that resolves to the output of the write method. - * */ - public async write( - args: BaseInvocation & { method: M; methodArgs: WriteMapping[M]["input"] } - ): Promise { - return (await this.invokeContract( - args as SorobanInvokeArgs - )) as Promise; - } - - /** - * Builds an authorization payload for a UTXO burn. - * - * @param {Uint8Array} utxo - The UTXO to be burned. - * @param {bigint} amount - The amount to be burned. - * - * @returns {Uint8Array} The payload for the burn operation. - * - * */ - public buildBurnPayload(args: { - utxo: Uint8Array; - amount: bigint; - }): Uint8Array { - const { utxo, amount } = args; - - const action = SimplePayloadAction.BURN; - const prefix = Buffer.from(action); // 4 bytes - - // For an i128, we need 16 bytes in little-endian order. - const amountBytes = bigintToLE(amount, 16); - // Concatenate prefix, utxo, and amountBytes. - const payload = Buffer.concat([ - prefix, - Buffer.from(utxo), - Buffer.from(amountBytes), - ]); - return new Uint8Array(payload); - } - - /** - * Builds an authorization payload for a UTXO withdrawal. - * - * @param {Uint8Array} utxo - The UTXO to be withdrawn. - * @param {bigint} amount - The amount to be withdrawn. - * - * @returns {Uint8Array} The payload for the withdraw operation. - * - * */ - public buildWithdrawPayload(args: { - utxo: Uint8Array; - amount: bigint; - }): Uint8Array { - return this.buildBurnPayload(args); - } - - /** - * Builds an authorization payload payload for a UTXO bundle transaction. - * - * @param {Bundle} args.bundle - The bundle containing spend and create UTXOs. - * @param {BundlePayloadAction | string} args.action - The action to be performed on the bundle. Can be a standardized action or a custom string. - * - * @returns {Uint8Array} The payload for the bundle operation. - * - * */ - public buildBundlePayload(args: { - bundle: Bundle; - action: BundlePayloadAction | string; - }): Uint8Array { - const { bundle, action } = args; - - const encoder = new TextEncoder(); - // "BUNDLE" is 6 bytes as an ASCII string. - const encodedPrefix = encoder.encode("BUNDLE"); - const parts: Uint8Array[] = [encodedPrefix]; - - // Append standardized bundle action or custom - const encodedAction = encoder.encode(action); - parts.push(encodedAction); - - // Append each spend UTXO (assumed to be 65 bytes each) - for (const utxo of bundle.spend) { - parts.push(new Uint8Array(utxo)); - } - - // For each create tuple, append the 65-byte UTXO and the amount in little-endian (8 bytes) - for (const [utxo, amount] of bundle.create) { - parts.push(new Uint8Array(utxo)); - const amountBytes = bigintToLE(amount, 16); - parts.push(amountBytes); - } - - // Concatenate all parts into one Uint8Array - const payloadBuffer = Buffer.concat(parts); - return new Uint8Array(payloadBuffer); - } - - // /** - // * Derives a UTXO keypair using the specified root and index. - // * - // * @param {StellarDerivationRoot} root - The root for the derivation. Generally the Stellar secret key for the master account. - // * @param {StellarDerivationIndex} index - The index for the derivation. Generated based on the derived sequence. - // * - // * @returns {Promise} A promise that resolves to the derived UTXO keypair. - // * */ - // public async deriveUtxoKeypair( - // root: StellarDerivationRoot, - // index: StellarDerivationIndex - // ): Promise { - // const derivationSeed = this.derivator.generatePlainTextSeed(root, index); - // const hashedSeed = await this.derivator.hashSeed(derivationSeed); - // return deriveP256KeyPairFromSeed(hashedSeed); - // } - // - // This will be incorporated by the UTX-BASED-ACCOUNT based on the context provided by this pool. -} diff --git a/src/pool/pool-engine/index.unit.test.ts b/src/pool/pool-engine/index.unit.test.ts deleted file mode 100644 index 2fc0ea4..0000000 --- a/src/pool/pool-engine/index.unit.test.ts +++ /dev/null @@ -1,207 +0,0 @@ -// deno-lint-ignore-file require-await no-explicit-any -import { - assertEquals, - assertExists, -} from "https://deno.land/std@0.207.0/assert/mod.ts"; -import { Buffer } from "buffer"; -import { BundlePayloadAction, SimplePayloadAction } from "./types.ts"; -import { StellarDerivator } from "../../derivation/index.ts"; - -// Import the StellarPlus first to use its network config -import { StellarPlus } from "stellar-plus"; -// Import the actual class after importing StellarPlus -import { PoolEngine } from "./index.ts"; -import { loadContractWasm } from "../../../test/helpers/load-wasm.ts"; - -// Store the original ContractEngine for restoration after tests -const OriginalContractEngine = StellarPlus.Core.ContractEngine; - -// Create a minimal mock that extends the actual ContractEngine -// This ensures type compatibility while allowing us to override behavior -class MockContractEngine extends OriginalContractEngine { - constructor(args: any) { - super(args); - // Mock implementation details - this.loadSpecFromWasm = async () => Promise.resolve(); - this.deploy = async () => Promise.resolve({} as any); - this.readFromContract = async () => Promise.resolve({}); - this.invokeContract = async () => Promise.resolve({} as any); - } -} - -// Apply the mock -StellarPlus.Core.ContractEngine = - MockContractEngine as typeof OriginalContractEngine; - -// Mock the StellarDerivator -StellarDerivator.prototype.withNetworkAndContract = function () { - return this; -}; - -const wasmBinary = loadContractWasm("privacy_pool"); - -// Use StellarPlus's built-in TestNet() function for network configuration -const testAssetContractId = - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"; - -Deno.test("PoolEngine", async (t) => { - await t.step("constructor should initialize properly", async () => { - const poolEngine = new PoolEngine({ - networkConfig: StellarPlus.Network.TestNet(), - wasm: wasmBinary, - assetContractId: testAssetContractId, - }); - - assertExists(poolEngine); - assertEquals(poolEngine.assetContractId, testAssetContractId); - assertExists(poolEngine.derivator); - }); - - await t.step( - "create() method should initialize and load WASM spec", - async () => { - const poolEngine = await PoolEngine.create({ - networkConfig: StellarPlus.Network.TestNet(), - wasm: wasmBinary, - assetContractId: testAssetContractId, - }); - - assertExists(poolEngine); - assertEquals(poolEngine.assetContractId, testAssetContractId); - assertExists(poolEngine.derivator); - } - ); - - await t.step( - "buildBurnPayload() should create correctly formatted payload", - async () => { - const poolEngine = new PoolEngine({ - networkConfig: StellarPlus.Network.TestNet(), - wasm: wasmBinary, - assetContractId: testAssetContractId, - }); - - const utxo = new Uint8Array(32).fill(5); - const amount = BigInt(1000); - - const payload = poolEngine.buildBurnPayload({ utxo, amount }); - - // Validate payload structure - // First 4 bytes should be "BURN" - const prefix = new TextEncoder().encode(SimplePayloadAction.BURN); - assertEquals(payload.slice(0, 4).toString(), prefix.toString()); - - // Next 32 bytes should be the UTXO - for (let i = 0; i < 32; i++) { - assertEquals(payload[i + 4], utxo[i]); - } - - // Check total length (4 bytes prefix + 32 bytes UTXO + 16 bytes amount) - assertEquals(payload.length, 4 + 32 + 16); - } - ); - - await t.step( - "buildWithdrawPayload() should create correctly formatted payload", - async () => { - const poolEngine = new PoolEngine({ - networkConfig: StellarPlus.Network.TestNet(), - wasm: wasmBinary, - assetContractId: testAssetContractId, - }); - - const utxo = new Uint8Array(32).fill(6); - const amount = BigInt(2000); - - const payload = poolEngine.buildWithdrawPayload({ utxo, amount }); - - // Since buildWithdrawPayload delegates to buildBurnPayload, we expect the same format - const burnPayload = poolEngine.buildBurnPayload({ utxo, amount }); - assertEquals(payload.toString(), burnPayload.toString()); - } - ); - - await t.step( - "buildBundlePayload() should create correctly formatted payload for TRANSFER", - async () => { - const poolEngine = new PoolEngine({ - networkConfig: StellarPlus.Network.TestNet(), - wasm: wasmBinary, - assetContractId: testAssetContractId, - }); - - // Create test data - const spendUtxo1 = Buffer.alloc(65, 10); - const spendUtxo2 = Buffer.alloc(65, 11); - const createUtxo1 = Buffer.alloc(65, 12); - const createUtxo2 = Buffer.alloc(65, 13); - const amount1 = BigInt(3000); - const amount2 = BigInt(4000); - const signature1 = Buffer.alloc(65, 14); - const signature2 = Buffer.alloc(65, 15); - - const bundle = { - spend: [spendUtxo1, spendUtxo2], - create: [ - [createUtxo1, amount1], - [createUtxo2, amount2], - ] as Array, - signatures: [signature1, signature2], - }; - - const payload = poolEngine.buildBundlePayload({ - bundle, - action: BundlePayloadAction.TRANSFER, - }); - - // Verify payload structure - // "BUNDLE" prefix (6 bytes) + action ("TRANSFER") (8 bytes) + spend UTXOs + create UTXOs with amounts - const expectedLength = 6 + 8 + 2 * 65 + 2 * (65 + 16); - assertEquals(payload.length, expectedLength); - - // Check prefix - const prefix = new TextEncoder().encode("BUNDLE"); - for (let i = 0; i < prefix.length; i++) { - assertEquals(payload[i], prefix[i]); - } - - // Check action - const action = new TextEncoder().encode(BundlePayloadAction.TRANSFER); - for (let i = 0; i < action.length; i++) { - assertEquals(payload[i + prefix.length], action[i]); - } - } - ); - - await t.step( - "buildBundlePayload() should support custom actions", - async () => { - const poolEngine = new PoolEngine({ - networkConfig: StellarPlus.Network.TestNet(), - wasm: wasmBinary, - assetContractId: testAssetContractId, - }); - - const bundle = { - spend: [Buffer.alloc(65, 20)], - create: [[Buffer.alloc(65, 21), BigInt(5000)]] as Array< - readonly [Buffer, bigint] - >, - signatures: [Buffer.alloc(65, 22)], - }; - - const customAction = "CUSTOM_ACTION"; - const payload = poolEngine.buildBundlePayload({ - bundle, - action: customAction, - }); - - // Verify custom action is encoded - const prefixLength = new TextEncoder().encode("BUNDLE").length; - const encodedAction = new TextEncoder().encode(customAction); - for (let i = 0; i < encodedAction.length; i++) { - assertEquals(payload[i + prefixLength], encodedAction[i]); - } - } - ); -}); diff --git a/src/pool/pool-engine/types.ts b/src/pool/pool-engine/types.ts deleted file mode 100644 index 985da7e..0000000 --- a/src/pool/pool-engine/types.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { Buffer } from "buffer"; -import type { i128 } from "stellar-plus/lib/stellar-plus/types"; -import type { NetworkConfig } from "stellar-plus/lib/stellar-plus/network"; -import { StellarSmartContractId } from "../../utils/types/stellar.types.ts"; -export type PoolEngineConstructorArgs = { - networkConfig: NetworkConfig; - wasm: Buffer; - assetContractId: string; - poolContractId?: StellarSmartContractId; -}; - -export interface Bundle { - create: Array; - signatures: Array; - spend: Array; -} - -export const enum ReadMethods { - admin = "admin", - supply = "supply", - balance = "balance", - balances = "balances", - provider_balance = "provider_balance", - is_provider = "is_provider", -} - -export const enum WriteMethods { - deposit = "deposit", - withdraw = "withdraw", - transfer = "transfer", - delegated_transfer_utxo = "delegated_transfer_utxo", - delegated_transfer_bal = "delegated_transfer_bal", - provider_withdraw = "provider_withdraw", - register_provider = "register_provider", - deregister_provider = "deregister_provider", -} - -export type ContractConstructorArgs = { admin: string; asset: string }; -export type AdminOutput = string; - -export type SupplyOutput = i128; - -export type BalanceInput = { utxo: Buffer }; -export type BalanceOutput = i128; - -export type BalancesInput = { utxos: Array }; -export type BalancesOutput = Array; - -export type ProviderBalanceInput = { provider: string }; -export type ProviderBalanceOutput = i128; -export type IsProviderInput = { provider: string }; -export type IsProviderOutput = boolean; - -export type DepositInput = { from: string; amount: i128; utxo: Buffer }; -export type WithdrawInput = { - to: string; - amount: i128; - utxo: Buffer; - signature: Buffer; -}; -export type TransferInput = { bundles: Array }; - -export type DelegatedTransferUtxoInput = { - bundles: Array; - provider: string; - delegate_utxo: Buffer; -}; - -export type DelegatedTransferBalInput = { - bundles: Array; - provider: string; -}; - -export type ProviderWithdrawInput = { provider: string; amount: i128 }; -export type RegisterProviderInput = { provider: string }; -export type DeregisterProviderInput = { provider: string }; - -export interface ReadMapping { - [ReadMethods.admin]: { input: object; output: AdminOutput }; - [ReadMethods.supply]: { input: object; output: SupplyOutput }; - [ReadMethods.balance]: { input: BalanceInput; output: BalanceOutput }; - [ReadMethods.balances]: { input: BalancesInput; output: BalancesOutput }; - [ReadMethods.provider_balance]: { - input: ProviderBalanceInput; - output: ProviderBalanceOutput; - }; - [ReadMethods.is_provider]: { - input: IsProviderInput; - output: IsProviderOutput; - }; -} - -export interface WriteMapping { - [WriteMethods.deposit]: { - input: DepositInput; - output: object; - }; - [WriteMethods.withdraw]: { - input: WithdrawInput; - output: object; - }; - [WriteMethods.transfer]: { - input: TransferInput; - output: object; - }; - [WriteMethods.delegated_transfer_utxo]: { - input: DelegatedTransferUtxoInput; - output: object; - }; - [WriteMethods.delegated_transfer_bal]: { - input: DelegatedTransferBalInput; - output: object; - }; - [WriteMethods.provider_withdraw]: { - input: ProviderWithdrawInput; - output: object; - }; - [WriteMethods.register_provider]: { - input: RegisterProviderInput; - output: object; - }; - [WriteMethods.deregister_provider]: { - input: DeregisterProviderInput; - output: object; - }; -} - -export enum BundlePayloadAction { - DELEGATED_TRANSFER = "DELEGATED_TRANSFER", - TRANSFER = "TRANSFER", -} - -export enum SimplePayloadAction { - BURN = "BURN", -} diff --git a/src/pool/types.ts b/src/pool/types.ts deleted file mode 100644 index 4f736a1..0000000 --- a/src/pool/types.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./pool-engine/types.ts"; From 7b3425049d85ae435d38667b4439d1b819ef634a Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 08:32:54 -0300 Subject: [PATCH 15/90] feat: Update dependencies in deno.json to remove stellar-plus and add colibri --- deno.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deno.json b/deno.json index 083d891..1df652d 100644 --- a/deno.json +++ b/deno.json @@ -25,8 +25,8 @@ "jsr:@noble/hashes/sha256": "jsr:@noble/hashes/sha256@latest", "jsr:@noble/hashes/hkdf": "jsr:@noble/hashes/hkdf@latest", "jsr:@noble/curves/abstract/modular": "jsr:@noble/curves/abstract/modular@latest", - "stellar-plus": "npm:stellar-plus@^0.14.1", - + "@colibri/core": "jsr:@colibri/core@^0.4.0", + "@stellar/stellar-sdk": "npm:@stellar/stellar-sdk@^14.0.0", "tslib": "npm:tslib@2.5.0", "buffer": "npm:buffer@6.0.3", "asn1js": "npm:asn1js@3.0.5" From 29856cdcf2531a5b3a951d5725c25ab6e2dc6d36 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 08:33:04 -0300 Subject: [PATCH 16/90] feat: Implement PrivacyChannel and Channel functionality with constants and types - Added constants for AuthReadMethods and AuthInvokeMethods in `src/channel-auth/constants.ts`. - Introduced `PrivacyChannel` class in `src/privacy-channel/index.ts` to manage channel interactions. - Created types for channel operations in `src/privacy-channel/types.ts`, including read and invoke methods. - Established contract specifications using `Spec` for both authentication and channel operations. --- src/channel-auth/constants.ts | 32 ++++++++ src/channel-auth/index.ts | 95 ++++++++++++++++++++++++ src/channel-auth/types.ts | 48 ++++++++++++ src/privacy-channel/constants.ts | 52 +++++++++++++ src/privacy-channel/index.ts | 122 +++++++++++++++++++++++++++++++ src/privacy-channel/types.ts | 75 +++++++++++++++++++ 6 files changed, 424 insertions(+) create mode 100644 src/channel-auth/constants.ts create mode 100644 src/channel-auth/index.ts create mode 100644 src/channel-auth/types.ts create mode 100644 src/privacy-channel/constants.ts create mode 100644 src/privacy-channel/index.ts create mode 100644 src/privacy-channel/types.ts diff --git a/src/channel-auth/constants.ts b/src/channel-auth/constants.ts new file mode 100644 index 0000000..d502fe7 --- /dev/null +++ b/src/channel-auth/constants.ts @@ -0,0 +1,32 @@ +import { Spec } from "@stellar/stellar-sdk/contract"; + +export const enum AuthReadMethods { + admin = "admin", + is_provider = "is_provider", +} + +export const enum AuthInvokeMethods { + set_admin = "set_admin", + upgrade = "upgrade", + add_provider = "add_provider", + remove_provider = "remove_provider", +} + +export const AuthSpec = new Spec([ + "AAAAAAAAAAAAAAAFYWRtaW4AAAAAAAAAAAAAAQAAABM=", + "AAAAAAAAAAAAAAAJc2V0X2FkbWluAAAAAAAAAQAAAAAAAAAJbmV3X2FkbWluAAAAAAAAEwAAAAA=", + "AAAAAAAAAAAAAAAHdXBncmFkZQAAAAABAAAAAAAAAAl3YXNtX2hhc2gAAAAAAAPuAAAAIAAAAAA=", + "AAAAAAAAAAAAAAANX19jb25zdHJ1Y3RvcgAAAAAAAAEAAAAAAAAABWFkbWluAAAAAAAAEwAAAAA=", + "AAAAAAAAAAAAAAALaXNfcHJvdmlkZXIAAAAAAQAAAAAAAAAIcHJvdmlkZXIAAAATAAAAAQAAAAE=", + "AAAAAAAAAAAAAAAMYWRkX3Byb3ZpZGVyAAAAAQAAAAAAAAAIcHJvdmlkZXIAAAATAAAAAA==", + "AAAAAAAAAAAAAAAPcmVtb3ZlX3Byb3ZpZGVyAAAAAAEAAAAAAAAACHByb3ZpZGVyAAAAEwAAAAA=", + "AAAAAAAAAAAAAAAMX19jaGVja19hdXRoAAAAAwAAAAAAAAAHcGF5bG9hZAAAAAPuAAAAIAAAAAAAAAAKc2lnbmF0dXJlcwAAAAAH0AAAAApTaWduYXR1cmVzAAAAAAAAAAAACGNvbnRleHRzAAAD6gAAB9AAAAAHQ29udGV4dAAAAAABAAAD6QAAA+0AAAAAAAAH0AAAAAlBdXRoRXJyb3IAAAA=", + "AAAABAAAAAAAAAAAAAAABUVycm9yAAAAAAAADAAAAAAAAAAGQmFkQXJnAAAAAAADAAAAAAAAABFVbmV4cGVjdGVkVmFyaWFudAAAAAAAAAQAAAAAAAAAEE1pc3NpbmdTaWduYXR1cmUAAAAFAAAAAAAAAA5FeHRyYVNpZ25hdHVyZQAAAAAABgAAAAAAAAAWSW52YWxpZFNpZ25hdHVyZUZvcm1hdAAAAAAABwAAAAAAAAAaVW5zdXBwb3J0ZWRTaWduYXR1cmVGb3JtYXQAAAAAAAgAAAAAAAAAEk1pc21hdGNoZWRDb250cmFjdAAAAAAACQAAAAAAAAARVW5zdXBwb3J0ZWRTaWduZXIAAAAAAAAKAAAAAAAAAAxOb0NvbmRpdGlvbnMAAAALAAAAAAAAABFVbmV4cGVjdGVkQ29udGV4dAAAAAAAAAwAAAAAAAAAEFNpZ25hdHVyZUV4cGlyZWQAAAANAAAAAAAAABdQcm92aWRlclRocmVzaG9sZE5vdE1ldAAAAAAO", + "AAAAAgAAAAAAAAAAAAAAD1Byb3ZpZGVyRGF0YUtleQAAAAABAAAAAQAAAAAAAAASQXV0aG9yaXplZFByb3ZpZGVyAAAAAAABAAAAEw==", + "AAAAAgAAAAAAAAAAAAAACUNvbmRpdGlvbgAAAAAAAAQAAAABAAAAAAAAAAZDcmVhdGUAAAAAAAIAAAPuAAAAQQAAAAsAAAABAAAAAAAAAApFeHREZXBvc2l0AAAAAAACAAAAEwAAAAsAAAABAAAAAAAAAAtFeHRXaXRoZHJhdwAAAAACAAAAEwAAAAsAAAABAAAAAAAAAA5FeHRJbnRlZ3JhdGlvbgAAAAAAAwAAABMAAAPqAAAD7gAAAEEAAAAL", + "AAAAAQAAAAAAAAAAAAAAEEF1dGhSZXF1aXJlbWVudHMAAAABAAAAAAAAAAEwAAAAAAAD7AAAB9AAAAAJU2lnbmVyS2V5AAAAAAAD6gAAB9AAAAAJQ29uZGl0aW9uAAAA", + "AAAAAQAAAAAAAAAAAAAAClNpZ25hdHVyZXMAAAAAAAEAAAAAAAAAATAAAAAAAAPsAAAH0AAAAAlTaWduZXJLZXkAAAAAAAPtAAAAAgAAB9AAAAAJU2lnbmF0dXJlAAAAAAAABA==", + "AAAAAgAAAAAAAAAAAAAACVNpZ25lcktleQAAAAAAAAMAAAABAAAAAAAAAARQMjU2AAAAAQAAA+4AAABBAAAAAQAAAAAAAAAHRWQyNTUxOQAAAAABAAAD7gAAACAAAAABAAAAAAAAAAhQcm92aWRlcgAAAAEAAAPuAAAAIA==", + "AAAAAgAAAAAAAAAAAAAACVNpZ25hdHVyZQAAAAAAAAQAAAABAAAAAAAAAARQMjU2AAAAAQAAA+4AAABAAAAAAQAAAAAAAAAHRWQyNTUxOQAAAAABAAAD7gAAAEAAAAABAAAAAAAAAAlTZWNwMjU2azEAAAAAAAABAAAD7gAAAEEAAAABAAAAAAAAAAlCTFMxMl8zODEAAAAAAAABAAAD7gAAAGA=", + "AAAAAQAAAAAAAAAAAAAAC0F1dGhQYXlsb2FkAAAAAAIAAAAAAAAACmNvbmRpdGlvbnMAAAAAA+oAAAfQAAAACUNvbmRpdGlvbgAAAAAAAAAAAAARbGl2ZV91bnRpbF9sZWRnZXIAAAAAAAAE", +]); diff --git a/src/channel-auth/index.ts b/src/channel-auth/index.ts new file mode 100644 index 0000000..dceb77f --- /dev/null +++ b/src/channel-auth/index.ts @@ -0,0 +1,95 @@ +import { + Contract, + type NetworkConfig, + type ContractId, + type TransactionConfig, +} from "@colibri/core"; +import { + type AuthInvokeMethods, + type AuthReadMethods, + AuthSpec, +} from "./constants.ts"; +import type { AuthInvoke, AuthRead } from "./types.ts"; + +export class PrivacyChannel { + private _client: Contract; + private _networkConfig: NetworkConfig; + + private constructor(networkConfig: NetworkConfig, authId: ContractId) { + this._networkConfig = networkConfig; + + this._client = Contract.create({ + networkConfig, + contractConfig: { contractId: authId, spec: AuthSpec }, + }); + } + + //========================================== + // Meta Requirement Methods + //========================================== + // + // + + private require(arg: "_client"): Contract; + private require(arg: "_networkConfig"): NetworkConfig; + private require(arg: "_client" | "_networkConfig"): Contract | NetworkConfig { + if (this[arg]) return this[arg]; + throw new Error(`Property ${arg} is not set in the Channel instance.`); + } + + //========================================== + // Getter Methods + //========================================== + // + // + + private getclient(): Contract { + return this.require("_client"); + } + + public getNetworkConfig(): NetworkConfig { + return this.require("_networkConfig"); + } + public getAuthId(): ContractId { + return this.getclient().getContractId(); + } + + //========================================== + // Read / Write Methods + //========================================== + // + // + /** + * Reads the contract state using the specified method and arguments. + * + * @param args + * @param {M} args.method - The read method to call. + * @param {AuthReadMethods[M]["input"]} args.methodArgs - The arguments for the read method. + * @returns {Promise} A promise that resolves to the output of the read method. + * */ + + public async read(args: { + method: M; + methodArgs: AuthRead[M]["input"]; + }): Promise { + return (await this.getclient().read(args)) as Promise< + AuthRead[M]["output"] + >; + } + + /** + * Invoke the contract state using the specified method and arguments. + * + * @param args + * @param {M} args.method - The write method to call. + * @param {AuthInvokeMethods[M]["input"]} args.methodArgs - The arguments for the write method. + * @returns {ReturnType} A promise that resolves to the invoke colibri response. + * */ + public async invoke(args: { + method: M; + methodArgs: AuthInvoke[M]["input"]; + config: TransactionConfig; + }): ReturnType { + return await this.invoke(args); + } +} diff --git a/src/channel-auth/types.ts b/src/channel-auth/types.ts new file mode 100644 index 0000000..8bcb9c8 --- /dev/null +++ b/src/channel-auth/types.ts @@ -0,0 +1,48 @@ +import type { ContractId, Ed25519PublicKey } from "@colibri/core"; +import type { Buffer } from "node:buffer"; +import type { AuthInvokeMethods, AuthReadMethods } from "./constants.ts"; + +export type ContractConstructorArgs = { + admin: Ed25519PublicKey | ContractId; +}; + +export type AdminOutput = Ed25519PublicKey | ContractId; +export type IsProviderInput = { provider: Ed25519PublicKey | ContractId }; +export type IsProviderOutput = boolean; + +export type SetAdminInput = { new_admin: Ed25519PublicKey | ContractId }; +export type UpgradeInput = { wasm_hash: string }; +export type AddProviderInput = { provider: Ed25519PublicKey | ContractId }; +export type RemoveProviderInput = { provider: Ed25519PublicKey | ContractId }; + +export type None = object; + +export type AuthRead = { + [AuthReadMethods.admin]: { + input: None; + output: AdminOutput; + }; + [AuthReadMethods.is_provider]: { + input: IsProviderInput; + output: IsProviderOutput; + }; +}; + +export type AuthInvoke = { + [AuthInvokeMethods.set_admin]: { + input: SetAdminInput; + output: None; + }; + [AuthInvokeMethods.upgrade]: { + input: UpgradeInput; + output: None; + }; + [AuthInvokeMethods.add_provider]: { + input: AddProviderInput; + output: None; + }; + [AuthInvokeMethods.remove_provider]: { + input: RemoveProviderInput; + output: None; + }; +}; diff --git a/src/privacy-channel/constants.ts b/src/privacy-channel/constants.ts new file mode 100644 index 0000000..f5779d8 --- /dev/null +++ b/src/privacy-channel/constants.ts @@ -0,0 +1,52 @@ +import { Spec } from "@stellar/stellar-sdk/contract"; + +export const enum ChannelReadMethods { + asset = "asset", + supply = "supply", + admin = "admin", + auth = "auth", + utxo_balance = "utxo_balance", + utxo_balances = "utxo_balances", +} + +export const enum ChannelInvokeMethods { + transact = "transact", + set_admin = "set_admin", + upgrade = "upgrade", + set_auth = "set_auth", +} + +export const ChannelSpec = new Spec([ + "AAAAAAAAAAAAAAAFYWRtaW4AAAAAAAAAAAAAAQAAABM=", + "AAAAAAAAAAAAAAAJc2V0X2FkbWluAAAAAAAAAQAAAAAAAAAJbmV3X2FkbWluAAAAAAAAEwAAAAA=", + "AAAAAAAAAAAAAAAHdXBncmFkZQAAAAABAAAAAAAAAAl3YXNtX2hhc2gAAAAAAAPuAAAAIAAAAAA=", + "AAAAAAAAAAAAAAAEYXV0aAAAAAAAAAABAAAAEw==", + "AAAAAAAAAAAAAAAIc2V0X2F1dGgAAAABAAAAAAAAAAhuZXdfYXV0aAAAABMAAAAA", + "AAAAAAAAAAAAAAAMdXR4b19iYWxhbmNlAAAAAQAAAAAAAAAEdXR4bwAAA+4AAABBAAAAAQAAAAs=", + "AAAAAAAAAAAAAAANdXR4b19iYWxhbmNlcwAAAAAAAAEAAAAAAAAABXV0eG9zAAAAAAAD6gAAA+4AAABBAAAAAQAAA+oAAAAL", + "AAAAAAAAAAAAAAANX19jb25zdHJ1Y3RvcgAAAAAAAAMAAAAAAAAABWFkbWluAAAAAAAAEwAAAAAAAAANYXV0aF9jb250cmFjdAAAAAAAABMAAAAAAAAABWFzc2V0AAAAAAAAEwAAAAA=", + "AAAAAAAAAAAAAAAFYXNzZXQAAAAAAAAAAAAAAQAAABM=", + "AAAAAAAAAAAAAAAGc3VwcGx5AAAAAAAAAAAAAQAAAAs=", + "AAAAAAAAAAAAAAAIdHJhbnNhY3QAAAABAAAAAAAAAAJvcAAAAAAH0AAAABBDaGFubmVsT3BlcmF0aW9uAAAAAA==", + "AAAAAgAAAAAAAAAAAAAAFVByaXZhY3lDaGFubmVsRGF0YUtleQAAAAAAAAIAAAAAAAAAAAAAAAVBc3NldAAAAAAAAAAAAAAAAAAABlN1cHBseQAA", + "AAAABAAAAAAAAAAAAAAABUVycm9yAAAAAAAABQAAAAAAAAAZUmVwZWF0ZWRBY2NvdW50Rm9yRGVwb3NpdAAAAAAAAGUAAAAAAAAAGlJlcGVhdGVkQWNjb3VudEZvcldpdGhkcmF3AAAAAABmAAAAAAAAAB9Db25mbGljdGluZ0NvbmRpdGlvbnNGb3JBY2NvdW50AAAAAGcAAAAAAAAADkFtb3VudE92ZXJmbG93AAAAAABoAAAAAAAAAB5CdW5kbGVIYXNDb25mbGljdGluZ0NvbmRpdGlvbnMAAAAAAGk=", + "AAAAAQAAAAAAAAAAAAAAEENoYW5uZWxPcGVyYXRpb24AAAAEAAAAAAAAAAZjcmVhdGUAAAAAA+oAAAPtAAAAAgAAA+4AAABBAAAACwAAAAAAAAAHZGVwb3NpdAAAAAPqAAAD7QAAAAMAAAATAAAACwAAA+oAAAfQAAAACUNvbmRpdGlvbgAAAAAAAAAAAAAFc3BlbmQAAAAAAAPqAAAD7QAAAAIAAAPuAAAAQQAAA+oAAAfQAAAACUNvbmRpdGlvbgAAAAAAAAAAAAAId2l0aGRyYXcAAAPqAAAD7QAAAAMAAAATAAAACwAAA+oAAAfQAAAACUNvbmRpdGlvbgAAAA==", + "AAAAAgAAAAAAAAAAAAAACUNvbmRpdGlvbgAAAAAAAAQAAAABAAAAAAAAAAZDcmVhdGUAAAAAAAIAAAPuAAAAQQAAAAsAAAABAAAAAAAAAApFeHREZXBvc2l0AAAAAAACAAAAEwAAAAsAAAABAAAAAAAAAAtFeHRXaXRoZHJhdwAAAAACAAAAEwAAAAsAAAABAAAAAAAAAA5FeHRJbnRlZ3JhdGlvbgAAAAAAAwAAABMAAAPqAAAD7gAAAEEAAAAL", + "AAAAAQAAAAAAAAAAAAAAEEF1dGhSZXF1aXJlbWVudHMAAAABAAAAAAAAAAEwAAAAAAAD7AAAB9AAAAAJU2lnbmVyS2V5AAAAAAAD6gAAB9AAAAAJQ29uZGl0aW9uAAAA", + "AAAAAQAAAAAAAAAAAAAAClNpZ25hdHVyZXMAAAAAAAEAAAAAAAAAATAAAAAAAAPsAAAH0AAAAAlTaWduZXJLZXkAAAAAAAPtAAAAAgAAB9AAAAAJU2lnbmF0dXJlAAAAAAAABA==", + "AAAAAgAAAAAAAAAAAAAACVNpZ25lcktleQAAAAAAAAMAAAABAAAAAAAAAARQMjU2AAAAAQAAA+4AAABBAAAAAQAAAAAAAAAHRWQyNTUxOQAAAAABAAAD7gAAACAAAAABAAAAAAAAAAhQcm92aWRlcgAAAAEAAAPuAAAAIA==", + "AAAAAgAAAAAAAAAAAAAACVNpZ25hdHVyZQAAAAAAAAQAAAABAAAAAAAAAARQMjU2AAAAAQAAA+4AAABAAAAAAQAAAAAAAAAHRWQyNTUxOQAAAAABAAAD7gAAAEAAAAABAAAAAAAAAAlTZWNwMjU2azEAAAAAAAABAAAD7gAAAEEAAAABAAAAAAAAAAlCTFMxMl8zODEAAAAAAAABAAAD7gAAAGA=", + "AAAAAQAAAAAAAAAAAAAAC0F1dGhQYXlsb2FkAAAAAAIAAAAAAAAACmNvbmRpdGlvbnMAAAAAA+oAAAfQAAAACUNvbmRpdGlvbgAAAAAAAAAAAAARbGl2ZV91bnRpbF9sZWRnZXIAAAAAAAAE", + "AAAAAQAAAAAAAAAAAAAACFV0eG9NZXRhAAAAAwAAAAAAAAAGYW1vdW50AAAAAAALAAAAAAAAAAlkcmF3ZXJfaWQAAAAAAAAEAAAAAAAAAAhzbG90X2lkeAAAAAQ=", + "AAAAAQAAAAAAAAAAAAAAC0RyYXdlclN0YXRlAAAAAAIAAAAAAAAADmN1cnJlbnRfZHJhd2VyAAAAAAAEAAAAAAAAAAluZXh0X3Nsb3QAAAAAAAAE", + "AAAAAgAAAAAAAAAAAAAADURyYXdlckRhdGFLZXkAAAAAAAACAAAAAQAAAAAAAAAGRHJhd2VyAAAAAAABAAAH0AAAAAlEcmF3ZXJLZXkAAAAAAAAAAAAAAAAAAAVTdGF0ZQAAAA==", + "AAAAAgAAAAAAAAAAAAAACVV0eG9TdGF0ZQAAAAAAAAIAAAABAAAAAAAAAAdVbnNwZW50AAAAAAEAAAALAAAAAAAAAAAAAAAFU3BlbnQAAAA=", + "AAAAAgAAAAAAAAAAAAAAD1VUWE9Db3JlRGF0YUtleQAAAAABAAAAAQAAAAAAAAAEVVRYTwAAAAEAAAPuAAAAIA==", + "AAAAAQAAAAAAAAAAAAAACURyYXdlcktleQAAAAAAAAEAAAAAAAAAAmlkAAAAAAAE", + "AAAABAAAAAAAAAAAAAAABUVycm9yAAAAAAAACAAAAAAAAAARVVRYT0FscmVhZHlFeGlzdHMAAAAAAAABAAAAAAAAAA9VVFhPRG9lc250RXhpc3QAAAAAAgAAAAAAAAAQVVRYT0FscmVhZHlTcGVudAAAAAMAAAAAAAAAEFVuYmFsYW5jZWRCdW5kbGUAAAAEAAAAAAAAABNJbnZhbGlkQ3JlYXRlQW1vdW50AAAAAAUAAAAAAAAAElJlcGVhdGVkQ3JlYXRlVVRYTwAAAAAABgAAAAAAAAARUmVwZWF0ZWRTcGVuZFVUWE8AAAAAAAAHAAAAAAAAAAxVVFhPTm90Rm91bmQAAAAI", + "AAAAAgAAAAAAAAAAAAAAD1VUWE9Db3JlRGF0YUtleQAAAAABAAAAAQAAAAAAAAAEVVRYTwAAAAEAAAPuAAAAIA==", + "AAAAAgAAAAAAAAAAAAAACVV0eG9TdGF0ZQAAAAAAAAIAAAABAAAAAAAAAAdVbnNwZW50AAAAAAEAAAALAAAAAAAAAAAAAAAFU3BlbnQAAAA=", + "AAAAAQAAAAAAAAAAAAAADkludGVybmFsQnVuZGxlAAAAAAADAAAAAAAAAAZjcmVhdGUAAAAAA+oAAAPtAAAAAgAAA+4AAABBAAAACwAAAAAAAAADcmVxAAAAB9AAAAAQQXV0aFJlcXVpcmVtZW50cwAAAAAAAAAFc3BlbmQAAAAAAAPqAAAD7gAAAEE=", + "AAAAAQAAAAAAAAAAAAAADVVUWE9PcGVyYXRpb24AAAAAAAACAAAAAAAAAAZjcmVhdGUAAAAAA+oAAAPtAAAAAgAAA+4AAABBAAAACwAAAAAAAAAFc3BlbmQAAAAAAAPqAAAD7QAAAAIAAAPuAAAAQQAAA+oAAAfQAAAACUNvbmRpdGlvbgAAAA==", + "AAAABAAAAAAAAAAAAAAABUVycm9yAAAAAAAABwAAAAAAAAARVVRYT0FscmVhZHlFeGlzdHMAAAAAAAABAAAAAAAAAA9VVFhPRG9lc250RXhpc3QAAAAAAgAAAAAAAAAQVVRYT0FscmVhZHlTcGVudAAAAAMAAAAAAAAAEFVuYmFsYW5jZWRCdW5kbGUAAAAEAAAAAAAAABNJbnZhbGlkQ3JlYXRlQW1vdW50AAAAAAUAAAAAAAAAElJlcGVhdGVkQ3JlYXRlVVRYTwAAAAAABgAAAAAAAAARUmVwZWF0ZWRTcGVuZFVUWE8AAAAAAAAH", +]); diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts new file mode 100644 index 0000000..174f907 --- /dev/null +++ b/src/privacy-channel/index.ts @@ -0,0 +1,122 @@ +import { + Contract, + type NetworkConfig, + type ContractId, + type TransactionConfig, +} from "@colibri/core"; +import { StellarDerivator } from "../derivation/stellar/index.ts"; +import type { StellarNetworkId } from "../derivation/stellar/stellar-network-id.ts"; +import { + type ChannelInvokeMethods, + type ChannelReadMethods, + ChannelSpec, +} from "./constants.ts"; +import type { ChannelInvoke, ChannelRead } from "./types.ts"; + +export class PrivacyChannel { + private _client: Contract; + private _authId: ContractId; + private _networkConfig: NetworkConfig; + private _derivator: StellarDerivator; + + private constructor( + networkConfig: NetworkConfig, + channelId: ContractId, + authId: ContractId + ) { + this._networkConfig = networkConfig; + + this._client = Contract.create({ + networkConfig, + contractConfig: { contractId: channelId, spec: ChannelSpec }, + }); + + this._authId = authId; + + this._derivator = new StellarDerivator().withNetworkAndContract( + networkConfig.networkPassphrase as StellarNetworkId, + channelId as ContractId + ); + } + + //========================================== + // Meta Requirement Methods + //========================================== + // + // + + private require(arg: "_client"): Contract; + private require(arg: "_authId"): ContractId; + private require(arg: "_networkConfig"): NetworkConfig; + private require(arg: "_derivator"): StellarDerivator; + private require( + arg: "_client" | "_authId" | "_networkConfig" | "_derivator" + ): Contract | ContractId | NetworkConfig | StellarDerivator { + if (this[arg]) return this[arg]; + throw new Error(`Property ${arg} is not set in the Channel instance.`); + } + + //========================================== + // Getter Methods + //========================================== + // + // + + private getclient(): Contract { + return this.require("_client"); + } + + public getAuthId(): ContractId { + return this.require("_authId"); + } + + public getNetworkConfig(): NetworkConfig { + return this.require("_networkConfig"); + } + public getDerivator(): StellarDerivator { + return this.require("_derivator"); + } + + public getChannelId(): ContractId { + return this.getclient().getContractId(); + } + + //========================================== + // Read / Write Methods + //========================================== + // + // + /** + * Reads the contract state using the specified method and arguments. + * + * @param args + * @param {M} args.method - The read method to call. + * @param {ChannelReadMethods[M]["input"]} args.methodArgs - The arguments for the read method. + * @returns {Promise} A promise that resolves to the output of the read method. + * */ + + public async read(args: { + method: M; + methodArgs: ChannelRead[M]["input"]; + }): Promise { + return (await this.getclient().read(args)) as Promise< + ChannelRead[M]["output"] + >; + } + + /** + * Invoke the contract state using the specified method and arguments. + * + * @param args + * @param {M} args.method - The write method to call. + * @param {ChannelInvokeMethods[M]["input"]} args.methodArgs - The arguments for the write method. + * @returns {ReturnType} A promise that resolves to the invoke colibri response. + * */ + public async invoke(args: { + method: M; + methodArgs: ChannelInvoke[M]["input"]; + config: TransactionConfig; + }): ReturnType { + return await this.invoke(args); + } +} diff --git a/src/privacy-channel/types.ts b/src/privacy-channel/types.ts new file mode 100644 index 0000000..5d5e90a --- /dev/null +++ b/src/privacy-channel/types.ts @@ -0,0 +1,75 @@ +import type { ContractId, Ed25519PublicKey } from "@colibri/core"; +import type { Buffer } from "node:buffer"; +import type { ChannelInvokeMethods, ChannelReadMethods } from "./constants.ts"; + +export type ContractConstructorArgs = { + admin: Ed25519PublicKey | ContractId; + auth_contract: ContractId; + asset: ContractId; +}; +export type AssetOutput = ContractId; +export type AuthOutput = ContractId; + +export type AdminOutput = Ed25519PublicKey | ContractId; + +export type SupplyOutput = bigint; + +export type UTXOBalanceInput = { utxo: Buffer }; +export type UTXOBalanceOutput = bigint; + +export type UTXOBalancesInput = { utxos: Array }; +export type UTXOBalancesOutput = Array; + +//TODO: Define 'op' type properly +export type TransactInput = { op: unknown }; +export type SetAdminInput = { new_admin: Ed25519PublicKey | ContractId }; +export type UpgradeInput = { wasm_hash: string }; +export type SetAuthInput = { new_auth: ContractId }; + +export type None = object; + +export type ChannelRead = { + [ChannelReadMethods.admin]: { + input: None; + output: AdminOutput; + }; + [ChannelReadMethods.asset]: { + input: None; + output: AssetOutput; + }; + [ChannelReadMethods.auth]: { + input: None; + output: AuthOutput; + }; + [ChannelReadMethods.supply]: { + input: None; + output: SupplyOutput; + }; + [ChannelReadMethods.utxo_balance]: { + input: UTXOBalanceInput; + output: UTXOBalanceOutput; + }; + [ChannelReadMethods.utxo_balances]: { + input: UTXOBalancesInput; + output: UTXOBalancesOutput; + }; +}; + +export type ChannelInvoke = { + [ChannelInvokeMethods.transact]: { + input: TransactInput; + output: None; + }; + [ChannelInvokeMethods.set_admin]: { + input: SetAdminInput; + output: None; + }; + [ChannelInvokeMethods.upgrade]: { + input: UpgradeInput; + output: None; + }; + [ChannelInvokeMethods.set_auth]: { + input: SetAuthInput; + output: None; + }; +}; From dbffeb05379c7ec3483d3ee22afa450b1874c8e8 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 08:37:11 -0300 Subject: [PATCH 17/90] doc: Enhance PrivacyChannel with detailed JSDoc comments for methods - Added JSDoc comments to the `require`, `getclient`, `getAuthId`, `getNetworkConfig`, `getDerivator`, and `getChannelId` methods. - Improved documentation clarity by specifying return types and potential errors for better developer understanding and usage. --- src/channel-auth/index.ts | 30 ++++++++++++++++++++++++ src/privacy-channel/index.ts | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/src/channel-auth/index.ts b/src/channel-auth/index.ts index dceb77f..bbf191e 100644 --- a/src/channel-auth/index.ts +++ b/src/channel-auth/index.ts @@ -30,6 +30,13 @@ export class PrivacyChannel { // // + /** + * Returns the required property if it is set, otherwise throws an error. + * + * @param arg - The name of the property to retrieve. + * @returns The value of the requested property. + * @throws {Error} If the requested property is not set. + * */ private require(arg: "_client"): Contract; private require(arg: "_networkConfig"): NetworkConfig; private require(arg: "_client" | "_networkConfig"): Contract | NetworkConfig { @@ -43,13 +50,35 @@ export class PrivacyChannel { // // + /** + * Returns the Contract client instance. + * + * @params None + * @returns {Contract} The Contract client instance. + * @throws {Error} If the client instance is not set. + * */ private getclient(): Contract { return this.require("_client"); } + /** + * Returns the NetworkConfig instance. + * + * @params None + * @returns {NetworkConfig} The NetworkConfig instance. + * @throws {Error} If the NetworkConfig instance is not set. + * */ public getNetworkConfig(): NetworkConfig { return this.require("_networkConfig"); } + + /** + * Returns the Contract ID of the auth contract. + * + * @params None + * @returns {ContractId} The Contract ID of the auth contract. + * @throws {Error} If the client instance is not set. + * */ public getAuthId(): ContractId { return this.getclient().getContractId(); } @@ -59,6 +88,7 @@ export class PrivacyChannel { //========================================== // // + /** * Reads the contract state using the specified method and arguments. * diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index 174f907..23051c8 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -45,6 +45,13 @@ export class PrivacyChannel { // // + /** + * Returns the required property if it is set, otherwise throws an error. + * + * @param arg - The name of the property to retrieve. + * @returns The value of the requested property. + * @throws {Error} If the requested property is not set. + * */ private require(arg: "_client"): Contract; private require(arg: "_authId"): ContractId; private require(arg: "_networkConfig"): NetworkConfig; @@ -62,21 +69,57 @@ export class PrivacyChannel { // // + /** + * Returns the Contract client instance. + * + * @params None + * @returns {Contract} The Contract client instance. + * @throws {Error} If the client instance is not set. + * */ private getclient(): Contract { return this.require("_client"); } + /** + * Returns the Auth contract ID. + * + * @params None + * @returns {ContractId} The Auth contract ID. + * @throws {Error} If the Auth contract ID is not set. + * */ public getAuthId(): ContractId { return this.require("_authId"); } + /** + * Returns the NetworkConfig instance. + * + * @params None + * @returns {NetworkConfig} The NetworkConfig instance. + * @throws {Error} If the NetworkConfig instance is not set. + * */ public getNetworkConfig(): NetworkConfig { return this.require("_networkConfig"); } + + /** + * Returns the StellarDerivator instance. + * + * @params None + * @returns {StellarDerivator} The StellarDerivator instance. + * @throws {Error} If the StellarDerivator instance is not set. + * */ public getDerivator(): StellarDerivator { return this.require("_derivator"); } + /** + * Returns the Contract ID of the privacy channel contract. + * + * @params None + * @returns {ContractId} The Contract ID of the privacy channel contract. + * @throws {Error} If the client instance is not set. + * */ public getChannelId(): ContractId { return this.getclient().getContractId(); } @@ -86,6 +129,7 @@ export class PrivacyChannel { //========================================== // // + /** * Reads the contract state using the specified method and arguments. * From 84c14870b4a9a050fd21f015e71483a57eb64034 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 12:08:54 -0300 Subject: [PATCH 18/90] fix: Update invoke method to use getclient for invoking contract calls instead of direct invocation --- src/channel-auth/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/channel-auth/index.ts b/src/channel-auth/index.ts index bbf191e..ec29906 100644 --- a/src/channel-auth/index.ts +++ b/src/channel-auth/index.ts @@ -119,7 +119,7 @@ export class PrivacyChannel { method: M; methodArgs: AuthInvoke[M]["input"]; config: TransactionConfig; - }): ReturnType { - return await this.invoke(args); + }) { + return await this.getclient().invoke(args); } } From d34193bbc840c9ec46a99ce39eb47f47a8fa93e5 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 12:10:37 -0300 Subject: [PATCH 19/90] =?UTF-8?q?fix:=20Rename=20getclient=20method=20to?= =?UTF-8?q?=20getClient=20for=20consistency=20and=20clarity=20in=20Privacy?= =?UTF-8?q?Channel=20class=20=F0=9F=9B=A0=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/channel-auth/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/channel-auth/index.ts b/src/channel-auth/index.ts index ec29906..5aa40c7 100644 --- a/src/channel-auth/index.ts +++ b/src/channel-auth/index.ts @@ -57,7 +57,7 @@ export class PrivacyChannel { * @returns {Contract} The Contract client instance. * @throws {Error} If the client instance is not set. * */ - private getclient(): Contract { + private getClient(): Contract { return this.require("_client"); } @@ -80,7 +80,7 @@ export class PrivacyChannel { * @throws {Error} If the client instance is not set. * */ public getAuthId(): ContractId { - return this.getclient().getContractId(); + return this.getClient().getContractId(); } //========================================== @@ -102,7 +102,7 @@ export class PrivacyChannel { method: M; methodArgs: AuthRead[M]["input"]; }): Promise { - return (await this.getclient().read(args)) as Promise< + return (await this.getClient().read(args)) as Promise< AuthRead[M]["output"] >; } @@ -120,6 +120,6 @@ export class PrivacyChannel { methodArgs: AuthInvoke[M]["input"]; config: TransactionConfig; }) { - return await this.getclient().invoke(args); + return await this.getClient().invoke(args); } } From b65e91613dcd90b848895559cd90abb38c71b84c Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 12:10:45 -0300 Subject: [PATCH 20/90] fix: Rename getclient method to getClient for consistency and update references in PrivacyChannel class --- src/privacy-channel/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index 23051c8..68f0ff1 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -76,7 +76,7 @@ export class PrivacyChannel { * @returns {Contract} The Contract client instance. * @throws {Error} If the client instance is not set. * */ - private getclient(): Contract { + private getClient(): Contract { return this.require("_client"); } @@ -121,7 +121,7 @@ export class PrivacyChannel { * @throws {Error} If the client instance is not set. * */ public getChannelId(): ContractId { - return this.getclient().getContractId(); + return this.getClient().getContractId(); } //========================================== @@ -143,7 +143,7 @@ export class PrivacyChannel { method: M; methodArgs: ChannelRead[M]["input"]; }): Promise { - return (await this.getclient().read(args)) as Promise< + return (await this.getClient().read(args)) as Promise< ChannelRead[M]["output"] >; } @@ -160,7 +160,7 @@ export class PrivacyChannel { method: M; methodArgs: ChannelInvoke[M]["input"]; config: TransactionConfig; - }): ReturnType { - return await this.invoke(args); + }) { + return await this.getClient().invoke(args); } } From 6b7633895e62d24244357e1a66ddd66f8c6833ce Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 12:30:24 -0300 Subject: [PATCH 21/90] fix: Rename PrivacyChannel class to ChannelAuth for consistency and clarity in channel authentication context --- src/channel-auth/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/channel-auth/index.ts b/src/channel-auth/index.ts index 5aa40c7..dac42c1 100644 --- a/src/channel-auth/index.ts +++ b/src/channel-auth/index.ts @@ -11,7 +11,7 @@ import { } from "./constants.ts"; import type { AuthInvoke, AuthRead } from "./types.ts"; -export class PrivacyChannel { +export class ChannelAuth { private _client: Contract; private _networkConfig: NetworkConfig; From 85811429d63c4371abe77b400b2cec7aa4fe0ba1 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 13:50:30 -0300 Subject: [PATCH 22/90] fix: Change constructor visibility from private to public in ChannelAuth class for accessibility --- src/channel-auth/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/channel-auth/index.ts b/src/channel-auth/index.ts index dac42c1..8cd4f54 100644 --- a/src/channel-auth/index.ts +++ b/src/channel-auth/index.ts @@ -15,7 +15,7 @@ export class ChannelAuth { private _client: Contract; private _networkConfig: NetworkConfig; - private constructor(networkConfig: NetworkConfig, authId: ContractId) { + public constructor(networkConfig: NetworkConfig, authId: ContractId) { this._networkConfig = networkConfig; this._client = Contract.create({ From 64561c20fd51388bd1ff629738182bfdd6375da4 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 13:50:36 -0300 Subject: [PATCH 23/90] fix: Change constructor visibility from private to public in PrivacyChannel class for improved accessibility --- src/privacy-channel/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index 68f0ff1..43c3ea1 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -19,7 +19,7 @@ export class PrivacyChannel { private _networkConfig: NetworkConfig; private _derivator: StellarDerivator; - private constructor( + public constructor( networkConfig: NetworkConfig, channelId: ContractId, authId: ContractId From 70464f093f1de3ddfae6751f5252a852be15c63f Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 14:01:49 -0300 Subject: [PATCH 24/90] =?UTF-8?q?fix:=20Rename=20ContractConstructorArgs?= =?UTF-8?q?=20to=20ChannelAuthConstructorArgs=20for=20clarity=20in=20chann?= =?UTF-8?q?el=20authentication=20context=20=F0=9F=94=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/channel-auth/types.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/channel-auth/types.ts b/src/channel-auth/types.ts index 8bcb9c8..8369575 100644 --- a/src/channel-auth/types.ts +++ b/src/channel-auth/types.ts @@ -1,8 +1,7 @@ import type { ContractId, Ed25519PublicKey } from "@colibri/core"; -import type { Buffer } from "node:buffer"; import type { AuthInvokeMethods, AuthReadMethods } from "./constants.ts"; -export type ContractConstructorArgs = { +export type ChannelAuthConstructorArgs = { admin: Ed25519PublicKey | ContractId; }; From 5006adc58d329d53472eff3d3d7c80e9afc81c65 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 14:01:55 -0300 Subject: [PATCH 25/90] =?UTF-8?q?fix:=20Rename=20ContractConstructorArgs?= =?UTF-8?q?=20to=20ChannelConstructorArgs=20for=20clarity=20in=20channel?= =?UTF-8?q?=20context=20=F0=9F=94=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/privacy-channel/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/privacy-channel/types.ts b/src/privacy-channel/types.ts index 5d5e90a..51419a4 100644 --- a/src/privacy-channel/types.ts +++ b/src/privacy-channel/types.ts @@ -2,7 +2,7 @@ import type { ContractId, Ed25519PublicKey } from "@colibri/core"; import type { Buffer } from "node:buffer"; import type { ChannelInvokeMethods, ChannelReadMethods } from "./constants.ts"; -export type ContractConstructorArgs = { +export type ChannelConstructorArgs = { admin: Ed25519PublicKey | ContractId; auth_contract: ContractId; asset: ContractId; From 448d56523eef787b8525da80816eb6b2551a3a4d Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 14:02:04 -0300 Subject: [PATCH 26/90] fix: Remove unused privacy_pool.wasm file to reduce clutter and improve project maintainability --- test/contracts/privacy_pool.wasm | Bin 17446 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100755 test/contracts/privacy_pool.wasm diff --git a/test/contracts/privacy_pool.wasm b/test/contracts/privacy_pool.wasm deleted file mode 100755 index 1f0fb93356ebb82bf0e49a2bc6d0e4b39ac8df68..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17446 zcmbVTdvIMxdEec0?nAngee9SZ>NC`|UYuYVojEO6iC7!2W1?d08*-H}3bu34LOJbV6>LWrZuo zv(zu2IHC3%l-VCec2uqz^?-o>3PaG$2EDBIXE>Ilmhr&3P|fkt@(EU)U`32-;9%aT zx9C|tqzr!dsYp-hvWoRRdR%4nK|O++CDdo|JA(QpT|v)#eE(sTm-JouozW9$oxv#n zvd@zAIiTw(A3!;a-w~8&0pI9pnd^W)4EP6R?2^74<->XkJrANaM*S$}J)mpKM0@H>$pkx<4*#%xwfGRGiUAbOr#l+K_!ww5-#Wl~q+(R_UV;wcT?cFq^W6 zYlr6-?lr1^-@b$MbBm3IV+R}a3u-tsHGOzyPK{(2j~zKOd#@VJP1lahFU~a7SpKy$ zjk~88re3Qia|fnor{)gURGRmtMODo=7N+JF@2oAT?Ju0J&DQRkYSgCph01-$8u!er zmtVR@3qYt_k|PWAOEc58g?$0`ts9zE&6#&zdf?d1?DW1+vG2&#y|eRE)9SVj3$?pu z78@8JMyWlQ0Ne$&w-;s>TYX*$)=c;A+c!PcnA%sHn~rp0-@dzRQ%8WtVl6T~y;`NZ z5Wo7jfAuxJ&mP+Sh&CD`die_eRB8{EqAR3mw(;L;Ym#@dsbs1q`D2#)RaF&DpDx%a z&DSmKWkm8PY_-wWD8F-izeejoeQTtOF{_fIuIi#GSY`Eil*i4CN6M-+soR*PD7AGr z^ZJ5Pg&#s3mOWCd>QKqFV3gK-de_tHt=R0Ysd zo!Q>6ur>@@RerHn7uG8hC3!OkRrPI!la3=?BnIg->3My$WYT|7*Y|^JY2gOYR_Lay z*FGdfw7Qx{DG$DAqOX%T5Lhun>0v4P5s~j6`eFg9`oL*)AydF0R**reaEo%&H~FtG z;6E9PsXhBkx_VweQ2nC1%`LKn(S;YfMT#g{ip>4QYjfdOI_hndyqEQ@_U%9eIHHn& ztLk|5y#G_TgBYW~4$;pBRd!K&nq7^MoI1xZiD%Oq#obCVNb zt3e+~r`Z;5vk?R~)x`{Y#8)X*%BJ5Tx$&4ych<>)w0DPpetg+gY(@o|-|Zh?bW?u@ zkK60T40(`EZ(<1q&8E9h0@thA2W{posdN&Ai_+yfFo;uKFUEbUzz)EC9WhsDtMpB2fvs z0w7Tf$cw?+L9;1+_|z(tZap6c22I}vOuGW{UaykBU@3VWe>WF)v>OaW1M!yuK9+iC4O?0W&C#qPqvvNVk3khVf_>c!Jhbd2@v+2Jzmn} z(JL_#Av(rHEw5AUB<8i$0T^?$&9ml7v7|bAgwu=hJ@SCuPUm->y}*kXzN%Z&T0T_D zc*V#-bYe&I@hGtwXJs;CWl~kl((+=;Y&vj5zX}z6ydHRGNW^R%qq1Bubtell6tiL~ zRrO6jdHRu0ee~NuRnjw!oWV-oA{f&VnO*$`ZDnshU{yWHsze?wVChpQS64ikM_Ejs zAP`kJ-ZGZZK`#{GE`y%rZ`mjLJN{w~40@^to-fE}=;1~9d+cXT^-W*-#Fzi?vnKhX zJX5&eD{x-V-_3yCv~(J_c-JZNr&XrgT=bRvcr!~cF#Fhku0tTEfRwTZ2hR`FV*34aqh58CW0Kp3n$ zMYEYxCnW}qA+A_t8?h{mes!6E2P_asO1Uk3#KOU4o7<9nN?@Agq$3IC_i?ELitG}* zDUwyG%-T%ysFWhFLXZ>Gc_m7p34zx07WtJmMSfA-!NqKJ*OJ@pbafdLL0ioqIE6E% zc#Dl7I;D*B57Gfk-C|jhhcRx7#Q$g=2d8jaw>uzkHC5@SRq`W}VzY%{k<+aCk`M<( z4;4RVMM<;m%Pb~u<8Ltx8K@)D7j<(Zl6om>f6muSv47_6#^zi$OeNOOtE?vfp&T9{ zG>1H-G6SQn(T!3{t;0lcIIV-fSB0ON*g4(i{^TiWLC01Al>RTbpsH?ePYl9f1YX#2 zxSfd)O+OE-hG~-rWx)t~QdKc!7d{Q*7FF%*f76Vjp7tBUocsrSl2Zd``nB1{k}dg& zJWf9+5qX@hyP@9{L@9Op6k{SEv{fm>HhC+70Q8x4Mk{VZ207`V8BM;+mVN`%EBJOb zr1~bHOPAk;OdiOf3hGKha_)$sKOZL;1hWR9I3m+sgp%3!Nm<-8p(x-xs zh==`@Zg+>2GjI8lw-ep;F^T$OFFfyHo%p;9WiSdN=VUNSbC88lCt|o6?upecVRZ0- zE1kto#vV?_+_fleLUCI8Ul+P>ZxBeITk`dRRfF zGig&M=h^Tr)a=rHbur4Gcjv*plBFhaRt!E6Ikj2UTqaUUr9_=ph$ zrb?W>MGKm}85I8^niK4VRUMJiH*}{XVSwW&m-$>^M%_@!DdDEUOhHGq(-BNQ9t{GA zpd&=v@0}R%0v)K!XE|ai6!t9ddSTyu3fjn6zjqi z@1S*hZ9T;vaHZ*>->i*UE0%S1lt`GdcON!tRfA0yY=X=U@e2E@H*w}beaNDQxnocV z?&qEIP`a`1ipgVq*foIdP#&f+Bza*#4ad&{b{0M?e((b8b*`QCwvF&aIK+k&C`WzZ zO6m__dQd;X&4`7%4E4xQAQ*}w$P4L(-=v;=*3lo0t~U9{z!19=!4RlENJ(LoDo+!l zxbGB&&L>a0&Ro$>&PBzOnK6fl_fG*Hpg8%%rW;XDp%500`g6gYc$fPjr1@m)6op-f z?J(kgdlWbYx-dW!15QEjWK}UvVbmBoGMrd7`8`%wniWCQ0;o2EP9&MJWb$woaR`>bNt z`-o47NM+ltHP&p}pNl!0^>U8P1Lgc~SbLCcKW=%E^I0%iHZAytPAc|z8b)uRrMmq2>2&8of2GN@_i9*nF=+iPk!iX5IgUr zEdvRQz;$bM1eO7}yNf-pW7-~jg*wDn!S6jJa{wa!oOgUV3`Kpn=R>sxBcFoBA>L1F z%Z?n{;rog?v6VUR1M^K5Mk_m0XkM&tRY` z>53R#cDaK9T2eHSre8PhAbhX!f{Ps%#twk7aNa~ggcTy{IKl|%Rxk*A!e)Leapu?U zIWU+RQjMKgGYgcnLh!ub4alO=Z-RUfsvI$x8bvo#bPp zZM5$aMD8r*Y=)L6%QhP`lyx@q8Z^oh?SfH>AiQ9;KY$ z&H+b4%X^@HKLUvMVGIZNzABqu{j zFVPEx1P>DfPru;jX9c=8v13>fR8%Eje{tb>XoPmo#OZ zafzn~Vq_P5Fff@tZ=`sQa|RF1g&|DDh&+<026TXAi0o*VC_8|wy2S@> zS;U7suJ&-wr_lXb)VUDDtZzm(Z*5C*wE`HZmfIdML{o@OqUZ!r0TphLoR(8&Ji1VY zMiwH4AD~A6%tZwK0OdmBy$IO=^}-$jrd8BG@?lPE)Ym$Kg%)1VCD$R+1fAB9U74s; zd|Bm)T~uOlim>n){9zGpRC!LsLjD+(S4c@Z{RRV|4b)!+{T1=2FH*^eWN>ShU9!Ea zrL9O(YexwRyGV2Mlt}}ULxWLKh)UhI0?0D*@kCUGw=mHghfs$5pmxI^05d_&%|F)X z61^}}F44;^Hzpx(D7RdMs6O&7LGdMZ5HO;-v(aw>+JGk?v$L-PdGu{tXfL2zK6xcU zY;zpQ87RsWJPGPj@c9w~k6>ekmI!!kbRXQmkkjSonu##NcZsmJ6UQz(zs*F9&YPL6 zwhRk++}s+0@-)}?L;3Y(0Uz#$sI7)kMY5TH#7mPB;H z-2?>EOj-ZPTiLn^(~xB{6iMtm>5({Ao~d2!&)9Zm!?W;=yC~_|$~d*9Ke{%~JesTm z-R6!V>=}7pigT^aXb)|nQ}PFFE4)m#>E9P+YkkJ)ijx>dlg?;YP4ld?i!te13IKur z-}_gQg~|KFX5|l?A^l`ZhrCyR4i*%_CK}1_%Bs4MxC|h=RnANO*}}`k{m4oUHs((- z9hPcXSFnS-xn#rrgo)D~M20wDMR-%NF^@N4_JC{KncRJt^+r;{|v! zpb*ocX=@O=gg$+xtXSBb#nubP9N=@fdiWS^vlweL*V8X>_>~HgM!I}6pvZwM2cSNm z?q^6ErvTinI^wD#SB1tHr2+!c_>j%j;jXf_hhcCJEIdwa5x}^U;yfpb)8&^7#Qi%q zV+PETb1Oi&1GuCC){(2DZ~8d8^1NOu(T;s^rXjeFwO^D zj1M_=re5Nzh-n?f(gAHgnD&RL`GEChK(SIrh~s#HC@w%jGvOe9cQj-#pdvY!oGk#` z)|3noQVkSAaRAVQhE$qnrSX*oj5SNxmmyfhArbILglMr5CpxPzjH_#=)BHN!nlRH{3_ORYY#h65ZjFxig@?x4>vbi+7w2LSP?2f`aTL_!7V6+YAE|0AdRzomr1sDWRF+P8s*Zrkx4@9hF@%>ACvuZ@ zuh>AwKvdSIu@iWTTaQ!FEX5lweMd_@#T@7hZUxD}`rG7-D%sZ3CSN?o;@n`z@p>sI zVwT^|^p@>Gbth0z#=T$xvMqxq{d^Hfl;t97gwrJw`Z92MEjl}u?zP!hKw@wMi?BK< zlPpOg7`3sumBht-pUAi9+5RT4}|R^hI~`S-xG}<<^ zL+)+q%_nkEgspNuoJir|*N0PF2rYP&{>rhETC@nX;ES<%R5aGDFl zje2m+q&;=3ux_x`W~%72f)L=Dk3=VI=J*+C&iV7v(eyvn9Hdb<(hoBcTz{!jzpiM& zPtYX2Yk3x?)phLdq5!stDXZr{{k50g4%%m-T`{Ex1j*tUF8!_|Qym(BSHfln^=I4G%cA`s>@Y8_Lm?lok3)dj+k-h}9D)tDGB5@??AZx(QL30`8=_+g$ zK(JpEZ!49+8*hi@w!qkdfiM*TbiLRMxG`+OQqkcC7D8Nnh>Hb*D%JV8LWjzWS7Dk=8EJC!R2S?#NoEv|DgQ+?8fT zVjxyhqR$Fy(pRK+>?8AFKbJs7+(e)IU{C}rw=%rgLwy$VLZ7=(Wk!qV-aa;R8ZEO6 zsyT3=wPIwAg&%Z78^ECz1XIL9MMSXADyngx?b`tWK+$8d#uP#;=|}kp??hNt*rco$ zedSM_e1mfoPG&n#x6ieG?|_EV!?j z9gr->-N*%gn=|nEa}y|t8&b&VoJo6W5CFVluEY&saUzRN5c0X-aU;@i;Xqpg%8EhH zP~3we4riFzdg6iA6^uvL3x@&bI)LF;Wn7MUx#)_ygLT;3Rm@H)A?;hi?BExyDVT=F ziF$I56XHk~hR?{XuVaOq)lYSUIk2aD*`Drg#J+$6B(@5~?p|jbP8V6S*hFsV zF2-+9gEkaax@kZZ1_BB#e!F%5TolOdX5I(yg>8lG!bq70UY0ohAjn;Z0j(HrZ*=WHRABgCQ;my;|U0a!{Z`tvy8iZ)|J6=6e^_93l>HH0l6vOmyX9y9U= z6O-NpTaEC`@I_a;AIVYbG}GXg&Es*cM?~Vj+)Q_ctnz&&o zLb4)!0JaR3*=}oU~M8Y;2Bnh z!iD5+(iy>IYC&nYH-$I36=({%vIv-)0LZE?%0W7~5Wd)tb;2C*pv}prHESn3evfio z*e%DEB5M$mdvu9kDD8i^R9qN=&6##q&`fsJFj>fm8-039=8Qaa6!yE(i=J zUJcp&!a>Mtla>%3Z-6O(FRg;jlA#1^9c!8==FtTw=5Odo$zu|~ zE#d@N3HeaM8-F+|NVjn0QZm<*S zFl>&HUE%?TTN!c!r5?Z}o3Kf{5vB)^4SY>8a1^cpDW;>PUT)qsX3rjCEkbizJ|=9K zyD>}RXn5q;SYX6OM+EobQc5<$?BYQLy0SfVR~Sg=w03V;Q?JmYfTbWFkgm z#eSulD}}YBs5Mur3Xf@xjV@XD3zE>H+YcAlzxl~~t7;H6;82_8v*?N3h9t)q$X+-8LtE%v5PWeBLdxR@p zT&y)xemA#UKAPLmHaa#oRv8-~n;4s{lq*A(;mSy5v@%wyRK_b4mC5n) z_|W+9_{jL^_}F-5e0+Rjd~%{ZF*GqeF)}eaF*Z?|7@wG!n4AQPlbC)IpeHeG5>;VY z=>on3yl(#R)XdzDx#`+H)%jy{(~CRimZoNBrgzL8Io5c^%;MtA++DX%%^s_5zXxCC zwH^-EYSXppn`aKsH2jG4U}I(;knri=+|=y$g@yTry>qpDj?@m~^T9p$9;vmScF+7y z?Pc?G_@wY)Drjc*sH?yB8-ppH>5Te$Z~V}AQmZLYCcJvMz;ts#h{ z#~ODF^ua6bR`p4ps4RH5iaVpozsH^YywYCYZ2K1K$luWBMqjR;MLmU}du*uh9*3^- z7v!b;`QX&-Y;D@^-&I>YHru%7nqzaXU6?vDxOKmspR-eQc0VTGZ$aK;H5r=6FRvIE z2(VOJSOf(HyFI(oTXtW+eb;r{Z{B`Gx@Y@!`$A21@2*>_dvCe!=Iwj;+;QvPxy2)( z&u;#x<C!&*7c{%!3Oxe7dozW`Se z*HyT#!F2M-=pArQvK@@=h(eu91} z2etjRzS?z6V3TRgOOIa`W=~TlWVp=Wei`Qi}fx$qn77QMQ0@8;1OA7cyHfLr%?Hm)VB0hfz? z$j$Zuvs-T1)(w;LK-sWt^nYu73ob2O6aUu2)wO_cYkf10#$RM?$Cu^LF@~^sg>|B+ za@S2Yrf#a;%QB5+_{&Ii5uV&VBf&;zUDm}OQWixy$4!E&zK8#2C zml+q+#kes|-n7rxvlqI(y8$P|SsX3_=bU#%17d zi2e963c#VMdjW25)24~BY1*1?9Erb7@O~ann;mi;@LPDr%v@tv?dY-Eq8R3|)^1tl zfNq%%>twYjCkwZ>G_*Imj9)O^zsb~I)d77;~A50xFd zdSdA6mW#|UEG|wTa?LLt9=^JK_4wt(LqlVg$*HN)$%7Nq6O#xU$EWW+I5l+VaP7e8 PowZ3snTT!=PN@F}7sgyz From 7bf39cc876c2e10140d041e0536a96b5a2379204 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 14:02:17 -0300 Subject: [PATCH 27/90] feat: Add new WASM files for channel authentication and privacy channel functionality --- test/contracts/channel_auth_contract.wasm | Bin 0 -> 17646 bytes test/contracts/privacy_channel.wasm | Bin 0 -> 20380 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100755 test/contracts/channel_auth_contract.wasm create mode 100755 test/contracts/privacy_channel.wasm diff --git a/test/contracts/channel_auth_contract.wasm b/test/contracts/channel_auth_contract.wasm new file mode 100755 index 0000000000000000000000000000000000000000..8f6ac2a45d583152be54112405ffe72db8d9ea14 GIT binary patch literal 17646 zcmds9dwd*Ky+7y7>?^ZLr=>uere${R)l{LTue2ayXQ72s9&JHH^qQv4w#_C@(qtQ| zw+F9@9&e zqc`i7_#V@Xm8*MnC)xwLOTL$&XNO)PFnuz*80{T;ne-EW3*b|jwM1YB<$IY*17}%A zyV0`+tE4ciM`h5z1MNOpu`j}00pyt8hVRXwfiO#@XFDKwqn!plo#+`53I}wzN;xll zLDtDSxh5~KbGhagcSdvC%eAEanX`^EGtv$|El#RQd9#l`CYNjReczkmd%izohKGO3 z7^>)4?H%js7Hy!dR^!hxKKhM1>L@p*hjk{CagnClH>gg^>>A!S($ia3&uZ@O-ZC^eS{>Q7r8+dCPWJjnyN5@H z#`=28BkI}tp5ET%_LLbTz%y1(dRFBzeS3LJe|OKW>UI@^>utHwa<$v8b8=I6cW+O% zr@K7Z>u9gLdwaQOxO;QYXxTBjyav3N?jNY^8Xar8rSAou8VX+(DF2<4las2b{CCM! zuWnZ8d^c}OkL!IUv!PijFVNm5On3^qCspY87xe;Z20DCj@*uiq`3E>;RspB(FbY_7 zpcgt>Ao~zUq&L*3H*1gRFj7EY=vN{{3OWc>$@p(4bV+aU^7j53-h01J88czVBLX`6 zi^k5g3ks$8Dv$~k?h1#OeXqqty~4@nwHK;_V^%q<0w?s>H0!_wPNifB=Y-ix!O1HR zGzBI!{(HHcU^NRJRn!scjbzBj*rY1B090M36=;Cp$HgrLSDa$n(H~UsdNf+F_Gc9)K2-uwndkjPD z*C*J~ZLu3*H`8J_N5Bpli{JnQG?6~;Q5%~v;O`a5;A?VtUx}B=JO)Y*R?^e1q>r$Y zj#i3c$VZ)&&~5pE0RT4)xR?^-a5uoBrZGG4I!!O=f-S%-aj}*fq{MoBAPqYsckt%H zgadt`6kJwvFAYr8@ZSm+RhxIFVOl7ef`K`~-Lgd}p~K|Qr328pU{LXtOE`t=X&9NV zbeU{P6-Zk^2k_dbU+5~%^5!Vw57b)PZ=m{Tc>WC>P+m#vaoCHjFH)dN05pwCvK+t= zXlPjZiKBS!;ZU3cD(EhOF1>V5(JAQ*#KD~XEk+qtBi8atiGv6=gL-kudrtXxT6XTz z4biPGxDdT8q4u}X`w+ZL_(O?#u8nzgMAZy47o&g(B;JXshd&j-Og$jL8Ubmk;XB3G zv-N;_3ZOMQL`~S9T5sbhV@)l9KBQq5E}}XFV#3_IW(9eJE(9!HNIC3wp$eyzy{y!7 zDWaa^g}RVNcj^SCika|Xh9cZ%!u1tKr?3f%m-{;>Y)2a{AroF3-Ogd$$%Lybg*5ri zgzMM>Lz!?RS{~L)Ulybf&_b|a;D!?wP?Lfk7E(6o9%ABeq(%w|L!h1!8U({~yz?3q z`Vo&(6p9<=j})|n{E=niAlW900D1KL$R9R&a28eV1C=BY zw?2Wi7SlF?WTFI;sZSt5I()wvlv9^HXuXVF;DulLBzM3d>lUtC6a>b>R{$0|tx0J2 zL%jx37$mk9Tv%Ev${ka&!;YuNt|RO*0I`3VIwN_49U~Sy>^I6CQ?Ubhdh7_VR$vjp zq1Z9iVmG26Xj39&(UBU6x?#vjQALQIP#BaL$3Gwq;#mE}1KFf|m{>SN`8Shue;>cw zyrl+PgSgs3xus$!_iqsaDb`OqKq(fxzxGboW*lnJG)n(Qf)$Oe>d23=HC>cI??<%8 zbQ_d)Ax=t&jmkT>!P9iKvbHNjMlA1G$YD09h$*%!qT2hY5SUbUQr96s8~+`aw-vw- zG{(POXcTEXpalt?e{1LZe-S`%rvvSOj5mlR-Y2y-v418ND7nR|j|eW~|52_|euk>U z{N<{Hlh)OU3$79!C2i?xKwQ&fuLpEZ4Hc0 z%&}x>*m1IVuvGX@aI9`XB0_F!#B$rbH0*dh4L>y#vtnO;Q1=&O%{t+4RV9&+)vu5a zK@T%}!c?L(!MNZobao>kx-lkB` zPK!up^J#U*{Ubhg^W-N ze5iVi+($b=_&j*nUCi6cCl^&dIaWT77ZVndmk)2BJcyV$wE}8l&5I>EgrW{}pHAu^ z6p~ZvsiP%ZM++e7teTucG}*9q4R!!_b~ZX%$nt0*%PRNqD}sS4WQg`C zMz_%EjLJJ;PK&Cp9nLS1X&_kw8Of7rz{FhfdKfwt#KK5q=M$6 zJfX_6e)da`<1=Gp8(5V-4QGrxH-LpqGY+zXEPN~vQZpbma#Zj+=_4G64mH@=nFd3y zKh;g4;3H(3nKcn0PWi8-82wlAyD$q5JhL&^NM4x@!NSpf>*ras%7Hc`>&_;r1rZ)% zm@c!l#))Mn^8$~~jyg2jVr|ePakedQZWpXmyFP3DQfI zR1jm6_E%i+UfjS7&xMF)XlPL zox+%417pV#D~uz+35-TI@ZfwBls7atGmy%KXYn{rfnuUTMC`m5%xR33Q6PZGg8T|+ zH!kz*OMBm!5-ft6Lqlx)A0TKGJ0WkO3O}X%ztZr5(5iqvwor_ePdM~Nn{Dh5m_pG} zPty~lyp`&Ar=>g}%#Q*yV%$8s8zK)v8*;+Q)p%mIM{KJTfrz;D+WVG3fQH zZA11)hj18I!bj#%l^~Bve-9!h9(C-&js;<08lLlVFzEyVRpkW7QE`N%yc#toPZ^F4 zTCD8X>BJ>p67po z-3oiz&>&Yiwd1HTgIhf3YDOu6Es#?UZ`zC4TtV?KJF^FMa!zy{ysm^IE#4W`9xl9D z*7R<2v=g7R;2hLlfbL1CjQ=18Y!XsDP)dac+l3&-15uX;3h7Y!-y&$>;`k1Sm$>9v z;ozLlo_D(Jnq5qH8MJ~Fj!1wp#rgjI@kE!$#_2$xq=;7Ym=w>a0>}RbAwnE85=oYa z$sGp3VoEqe(n0zJOi6{4|LN=uvty&7u7-cUoI-e}W`DLiin$I?pdH!k;G~+5{XodG zbRHKnff7>IvB-H~4aT@$G37r*0{m~rg!P zg#|bWgUE221U_)!kJ8pqraZ>tb58{A05t>Ez!Tx3l9NKRE2nZAbUN_?oJr1v^2&c3 zS4P>l-)jRmu_?ti<-di)-eN%s&yM-pjKn;3`IM)!LS-BOv!C>u1;Qgsy^LgSyZ65=5eFFr5m5BV;k@**#_w7 zL|wuY9yd*Qo{rAC1*cms0%$ywCNdsKL+o~=4~gLnnn568*0!j}+WGTK<*+y|7rA}o zlVua$El-wB=)o{4{nB$TU*WV$D~pzIW%mY39P&1*bqr++#IB)$JuN>1B zC>fmXgK(ne-nAyzHX;z0(Efv#srdMRNY1b&BEoT8kOok+;5a1y1oii;4#J+PI`lFi zX#X~%C>Zo50NlUbZu`YS816tw2tMbM8njn~I4jtDEe(lar)xn4#{Y_iT!**-=|QA1 zLAB)|QBq8qf_jP27_$`vVvIb03lJ$t0>y);3pJ<2;d>NgKq*9yQxN}aTv38ZL6d5T z!U|Pzm?U;QV05O=7@XJ`NR2fJEJ;D8`dK~V5f~mMin`^YKFJ0syP16Oz#9xo>ca&C zh{?34EE=>XoWbA_!M12uFnO7e=b$tz*j#%>rlSQ)+wDR*+_j<~sg#zbWD3gLcB^N! zT8r`|m0pg6iHLjj`}Q4&9MVZPQn(Zeux&v4Ul%C>E=#j@u}j5y3s=e3j8d^wpfzG) z!UQP@7%Gj*G!-Cw(pPF$x+BTf4T^E87#(F~>VvPqP%#h?AKoBZ!Xs+y@&*Edi&-1w z^0*V%s8ip^b%Pz$t51g$X96z-i>Y9YwX;Z0v)2}1D-6XUy<&UxR-K5C?jesmXjS#n zGMHnn6}Dvwq+t=*Qq1+Rbkqsm#v@=MF}`O&mGnivCxNQW|C03+@)_uU%Q2D%UXoB= z6NvV|LjZ39r>PL_NrQ4H4g%;xp_TH#D?p2)kU)YeMc6V6BOA2v2&Xp3pa2QPec+wM|470a6$RV}Ys~%MS#guhS7EWE}csLvk zVgW8NhyWJ|x)m3wdNx2%%5~sQVzUWTgGFd!3<^nMY&O_9Ls}0gG*3&KbmLTbJUUO7 zR)E$@X?38rOj@mIbwZt}APbJOik1VZL0dFog>vnO6h6hx@Ud=g!F5)_X$+rCr|>!r zJm*J*U`(yRx)4f%$pFZN2@ob!4uO-EQp>@aI#5iH7c-HLAdyOt*&n3%5*EJN{cp<( z06jhcT$*r*sfo@3lt-e$1%3~<(lQxh>@W-b@7u{J@klrO6u(^m$95omP*oP7Ims7S zTom0Bw!nT5e#PV0P3spqKT+9`xDdIs}B<+zzCmfa!DpsUbX){Sy$qL#0b(1 z*@E~8E^px;7C7{`@*c620R@6iXs?SKkKx^v#K5qvNLiksB-l_pgaV6#DZ%g!RNh*` z6D=wLiV}I#j{Ki+MW`R*$rl+_YLj7=oQ#5Mb*lMts*22YElIT)ZD^ybM!Ugd?rE3os(GH(}O3u*NnpU^EO; z@Q#|@gR{wl(c^fSJfusu`Gs9ah}Sq^O6mQ@3@#bu=S&#wyYzIAIRACMxbS;WqK}>_k4F(2x$ogCLX&(+MZ>&<(o= ziQG2aGgIJ99TlorxGNPZ9>rNGQLF)EB)XzmMYEcqf_1@w6xnkD@uYh$sw@Gu4Zn)F zGb|HGP{40F!O0~YpvDPHe}-J9V(7ju9i$*=*W0HXwtnfFnqU%;3HG7>Z$)4k@R7Fl zF^All=$%v)RwhWpwB_AYw(``CsEh9>WhlKVxLC2IYXS{2(Sv-EmkZI=QXRr`)tjvh ztu%{)MD`zYb#?;ogS|LhxPX%bm@X*c!Ug-}Aj_&WThPub3c3ED^^=HV73nXB!1X&Z ziQHQHG!TV*b{3lg+`_rRgFADtupCW*>p>jFJioF*o=|vzif6C^NJZ!gya1E?gUrri z_G}1`K@U$gQ$c+zuySVUBUIzT0&Agp3;0Z+cQ|9g?GW*?Bb@*oa~Un(*n zW|*uH#llNtgp@%#5HErwj=Iz&!Fi8XO7Rx=k1fl^rX*hb~csaKHpIwX+Mj4DMf zDT@RvZV<3WUC`UhOb4Ejl@N8TFg0N%dLT?h!o)av7U>ZdRNGl@(GWa(DG8N2YP_9b zLtwEQ=5fntr(ki!28clFW@#zUh19&KDWanS=>*{)uRJ)(Q(4^YHEZW4j^KiP&QRVL zHN%mj=^!L>U8)mi8;b)#xCdIe^-MV`C4TD|n74;VO9`h|d05|#ROd#jlcRd#2cys= zU;VSFkYPH zrNa#3Oam-|WI{C8K?Q|-6D4^dad__}_t-cgEhYEZfS~|L?$f+aC-)iNXX5(|EuOT& zR1toW2|yPEhTC7HM2l(AZ2bdEdbU(g9Vz!z%kkYAL!;F-gN=R9>#J@*6+gGKV@mlds}%lxH5=DS^WSq0zo7S`4vfVU2}bY8mZ zy7iyk^t0jY-i5C_^IiQPe&weQeCdln_|5MY&+^Ual8wb%H@>0t*vQv6pLkX4zJI&< zqW65_D{sH+@jra;N1GR%x-4+@?4S02uKM27UUq)vx^=g9yexP1uYdBupFVi^tG;$# z*T_?69@Clr#@L^?UVXtWYhG~MOZLxs)5^;qe(XPgcFi|F{<+Vb@skTL+rEGIs%%fI z>3GV@`#%5Zw_g3ojekAk<$Z5yy8am-`_n~VJ@dAgys@&gZGF?ylV&~r9}j)!-aG&H zj$gj`Z5LkotUKCnc;CR~dwzV@-7hR2@0`3TAGUw!(bxRrSNGrl-KQV$KC%5(Yp;3J z_(uo7_M$(YbJQtI7Hyb6+;Z*XpZv{tAO8CTbMHLq#^+zV`!!d*?)tr7`0y{^mH*NS zpE~DvLqC}G-=#j?`J=#H|1Yc73~$@?v3+m8>TmD)_RY_}tNkkzKYGz0K5Sk)>kq{* zFIiriz47AB7mWPk)_bpe|J5&l+2Ehf{mJoPYrgx3%kEn6&e?n2t<}?e7o49u?VxleZ#w3ATgS$ZUbF0iv%aY=JT(5>YmI%?+aI-_3o4R9sm9IPvwvM z%_)!n>jQfpe%p=ze&@Gd^OtLn9qbx^?m5*LUA5>Vr+lg9xAWEIEiG~M867Tf8S6y2 zh(FQqZR1;|(K@Pjhp?IaR#tjB&QETzd~x&}TDh;E!;aeTchRoRxfrpqdwb95cA4V* z8kazt$=8%$`^rE(zn<2q>oxo;7_;Iz+1gKqEnn=9$B4Y~_ruB9)Om!BKZpFt7y~iR zBh734DY8vR#F-iHdGWwdPcNDcGHDv^+cwx!-8E7k6|e{K-E>|J#Qml9$1BmFD~_yG?Mc<`HY z`7q`3lRsw&^LqQ3?WqN(c7E!3uah(nK<&q`5w&{vaNkIo%4Lbc&#`fxbN+UycKgsk z?>R%&v&%vlONO}2A?%D8_B8xTeO>v*?5nroS%=nO1Q8heNIkAxjS~S8J;ipLwl$9o zV2;>fh&%Ed!!$u>QAHHPNvx=c@cepW=79n%;@g`a**a z8tD?g<8@qC&zOVznhfHxOjK19XFML*HoH_dwiDY|izA1uE%8LAH0Xco+N&%c0i15_ z`te+?)EDp?ePJDrT(S!gpqPhEbub+8sN?mucv!RFrY-eeczAsh$IO8~{1*_r2CIDo z-2>&`ZLv?gYW_^1ksweW9NST@_SACWv7D$bt|iKb!PLm;Sd1jSc+tugi;@(Y9T^?% z?YF~SOO`I`T-15O$d*M*7A{@=}*2T+KF5PnClCG^=SFR}cEL*;H<;o>X SH?Q2%+1a~g*_O=+4gUodp~q7I literal 0 HcmV?d00001 diff --git a/test/contracts/privacy_channel.wasm b/test/contracts/privacy_channel.wasm new file mode 100755 index 0000000000000000000000000000000000000000..c911349c53fa939ef6d18f446f37c28bb88bd6f2 GIT binary patch literal 20380 zcmds9dypJQdGGGo$K1{B-N@!4%RFWmr|MKGTxZ#Om{b|f+VUeJNS17#l{&5NR`#7v zchcTTmJ3;TSdy(IwsU|=Sb@%uFzV7K+)tcR>l~Vd{{o-B0(W6K8(OpKqHE$LEN~q<1 znb)Igmzkeu3vXCL+dRM3E!fDAK(3d1wjM@npj*;+H~Mn+eSjHr_gA2Gx4u%~uM|9Q z!Tp3_I3XiX$Sk|{BoDjdrTjju*DDjeW@}kjs$pM$7!3r~ zYHd(HLvc8`Dhvm!K~xK>{ZU{H{+q!IR1la82Z#FlYC{)PUSPsO9hwW`!NC`_ss>(&dyiOMumQ*ya(zACKuGNS1P-S0x-%RIrqsp!EeG}7aTi|E>RX5j0N5^KSXXiQx$L3}_ z>J8=AEc&W9R%Q?G-#>M`dQ&*pX-&_z#^%(U!PD9P(b4hNTx+yFJs#+2boADCYd=Px zZ3m|RiMMS1tk!1!6$|*NbYyR%e2Ls9?+S+5MJC`wfGzVCnMouB;ZSN=l@WE#Ye za-LXbZa$-rHO-EiLEFgWB~mvO`5~J?qc~XnhwQ1MtF`t-#CsdYc1uj65*7pRZ(Kr24P(%CXL+`iH_E&L_OL}?rN5fC)(Y_?`G$yPsM*~C)%R*(Wa^!L+W+Y z#E-FlOAQmD@KzQYT4?y9+5uLe9*k@1x&h&2q8f@+G=!;u*&h(Rsc&>;3~(oF))&_CC!iG!gRm&R4 zy-{LK(g+LIjJ1G#D%9BNd7`w4wOR0}Xv~ zy_8f~x;PgV7*(R$2kDaXTz;*g4|e>5t|`YZX!ZNvz4(sP_kJpg|Amk;PCJOKqdYxdvSo6{j8aYkQ^zj z8)<|jBS=CaBk#T`{#;M@;d&o?B=r#9r!-Jn5kU$)XI240FjZy%0NG>(KnG>j#Iv`6 zlW{otA!&RCoMfN^P8v7`E8?Wjg%iSr-~@nfoY(`(yqr%kRNq6$?7>L}D&S;*6TEJ( zAcL*YjC1fQQ>b2?R_Y%g1R?r#R5LPrra zJV^CqBcRt+N&m(m!6jsD2GVp=;;ZP*0-BLZqK&~YE`1w=)fm7BN>HBrvaN_1##?E_ zG>;r<1WoC*WsgDG>X8;vCxe+kI@js6+&RD~o)oZh5ez^DFaR;`pf4!h4tEGAuVFtfZkg!D9+dI~K*I>X$Em=b z67@miSptXdXoM@G0x)twJ^h%`HF?D9%Ep)qK>@?bRNIuVuC1J@(ylQZ3~X$|8P(pv=R$o^uPwmn!%o83XLncEOvk!{$ML zrdr9~hC`hefWLrxl*K=GahE5#O zOBC3YQzKNwpC>&{6TKRZS+g}^wcLakN?2cE%=oXZ7v%GO- zAwoQ#B?J)2#K;Ik`oj1_Y_g^$W!X@uqb3U$kj5t%6pgoFczJC#B%?`|3h~_>3vo=j zB?DM!mkc^#1jm$tT{4K7l93STmn;Iz7lA;+ui^ObZ6$JZ+8t08Sct{pE3wkS3dZrT z*tx~Rb>?0ZLm|CQo5B)04|vwAU>VW1Kyv;E5eKM1=-wsE(kNqPp6kIAaCIc1hIs|< zB;0~pm~*$=&tm6BI(Da^+?e1}39cDSCGbkNa^U}vuZz-QN5Wgf>y*jWjNU5W%0M?~ zOcDvK@;P17u_%eRMnL+VjHYl13ibsuFuLlT?&NPpBv>I^pJENM4s3Hp(b31RguAa` z!=mbyWucU53qm>)G&pFJCSqpkf`+Gc9-2-WsVMfJG76s%f2`8SB!enJ3(z4g#b37C zu{K!HNQb!{WtXau*GUaC@2XkUUCUX(oYKzU7qzzPv=>`wPJ6*yYVRVnk8b)TuFQBJ zbJtV6A|+a2JGU{eGWX+FP#l>=AkgV44T^9*iEBUT*6C@(Wz)xrzu62rfz4^tgxzy8 zNq+%RT=f%X*eqxpe(V7K@jqDxq>C6%>f%q@nqjkOBspChJZKu;4w+7Zl1Bv7jlqKs zb5f8*g82tEW167QXVIe+fL&sdvu-c`3TJVqM$L|}18H{$fPXJ@icIl*gkcCD9p8Qm zW}Q}!^ylW*UoGR-CvtB2Y<_L+@ht4-8Pt(^6qL>>*#HQ8kXDB4BCdTd!qCBJx8a5sF&EU?T{r6Y zqG=3pa+t!};1N@Y5zut%To+U67Yt08rYhz-fZeA+Vf%?If)FOtG`Mh;#{jn=5#wWS zkjFSkAcMphLk2!&>NUg_35JI2|1ha`Q}#H<;2L6ySL;=t;7(87i2o3J(k0_J5v@ZI zr<3g@f`HRdWxJUOHv~7yke2i@A4chbD5ye)@nB3lgTuL^Kr>ax)YCwm*1=4prISyLYbQdv(p-X1S`5?PB#lUM5< z@4+oGt*e3%7J|tvcR3pfXTdMPoz!{yau#XAb)JZ)gZNiTl|*~sA0>l?j~xS6gML{l zSdFk#HX>U((R>DvP{v$T&xiiAAo%jkyEr2M3qIR`-Awa<*HY}2bLfE zFn|b)zEtWg44gI+26R#*_1}i|oM_IUK&h6fBUVah%#nsl25)-wNFzFNtf_W#)m$G; zqI%7)D!c7i@Pb!g%29Wh zdDh81VB7;K^?Xo+MTz;HHPBc01c^Bf_Bfl`J_@RH;gd*Z%hqJCh^U z!?BZ2&FJDC$L_JYtY{MTsi0wboXIq7(Q6o_RfrG_WR3@-*6{VB))EGwK1;oB|9q5~ z25jnZ&{U1H#=BBTHgRU`eaKJfReX1hEm%G+E^=dVmRGAsKMo3t4Y!!bUb;xJI(gzn z)D{CqUQ%qs8vT@gHm1|1<={&K4i}p)0`Onx7H|(hd!L6EIMK|3oXte))M#0L|0G3< z{Qh(<+AnvD77oK_+_K~l2M&T|xrPVsbopcnDAG(INKU$V=Mn;4Dc|*7&@~rKZ4b!c zYnBF^-(QPnkr)!e$k|Lt>vangJPKXG#Q^ax7gOLqeV$(^OB1(zvWX z;*8>Y7c$q|Q|#3OF9wnVCdaKzPnvg7AJm?Yxxr(*f3{_Gko3OQun6svT^29KoZh zZcd`@7sDx3lZfe%7Udf`mvklhfpjGg@hBH;fasn>ra7WcvcOavr&u)dL^w6)n7}c% zLE;6>$#z=)vF1FFkp!dyhZSy4%EVvLKAj0a(XUh=gaqLMhRuUwdAMC~u;Re9>&-*R znNtiHf1Og|qJjRD2VGn?jzrX!Fm0HA{jGE^&U4}k$T!#vW^t#;c%W)6IL^3c2~qJk z+1^+ce~VYk057ctY%$p9TEpfcXrrx{1rqcONb5$w+X-ug-XOUGh;S5yPev4$9Nc)3 zQz8R65TsM4#r@ei5de~73>K1M+LI#6pA=a%W3B?9u+~J)qPY%<&R17 zqayuL(x3EU`%Kmzmd&ygGE64-vjR-!av%kDIj@1W_D6)U*g!!xrt7M(TO|*mEDx9O zR-q5c=3W)D#bu%??p0Yx9P*Gf6I)f0bd?G!FIIW7;&`}DYSM-92KZ+%>lBP6emi+= zeH%dQ14tzK6gYym4pY2vE^lwRq=N@ zp9E$qB#wgE17}nT`lxGbG&j*n>r4}`=)e&T1>(gRqx2(Ue$GGOK~lH*i5x8Buos;3 zi$d`HvVteS_@oGEGXpZ2sc;V&BPqA@tK?TrEnq zq->W>rGaIBX-CcO z)ufE>xnBYxna%u@*-6Q-Aq=Ai!jr+f_z(Cmyl;khDfnUC1TQYbOF+!^WCu=9zr&3W0THP%PuB| zsRV1xP)rb`o&*v7WL4r)OP>r5Vx#Rh$#<$t`~kTWz}P)(|XFCifPtqgFspus|HC9tU{P7n&4sBE~u9tBV8>hyARlsb^onC?A z^7P8`^C}q7dq~)jKN}GpD-};>6psv$Ir+at4I7m`BEa(uF{9$4Iy?jzz;R!leo318 z^DPyHhX~*tkIu=-ZRja)fG1B1JVl{U3IZm8JadDk8qY6*d{RVHiznmn6PD*=b$Ui- ziHfse-1C`*(-ZJHW)by_C)4lBM1%Q6F+3>xueRjGSbz#uppl0oaG&sNI44XxNEeXx?&gVQPPLi4=Fq!Qb5Rgkdh6CG4q3q{%Bibq8HO>b^Hit$4eJj9OG-} z-bl8FUH$eb0#y8d!mNp6-rL8_zr~v9`mRGQfweEB0FWMz{oA)C0(c@vAp$VGB&W3i z+2Mrfqddb>d@rD2t5ipLH@g5f>5nA8_a`=SJedbO40>->`UN?8Kv{Z#zB!ml4_ zUGgv5Eb8p+1$5|fHJPgP(<=T?0Knm4%^t@P6>UsTvth4JssGnQs%b906w8$#E+Sl-~fusRJ;qYMKj-~`WniAL@ zH_0ry*eAFALXZIlq0_GQ4G(fq4f5mo2{QrFjo7_=grru=tKD%GU)}`w3w^K9;&26iI*-U=B?-NX-wUg%(RSNe(P`SI`A7Rxnw0$`vl z25IKfJo+vINacIXi&h-rwXdeuT%;z!F-ixa8azL+b6jS zPy6wXj+8$&Z!6aDrJZW}Zdbf$D?(m1-NHBZAe(Bsm3L5tYPth=csHwAxhtv2nh+Bv zCGHoOaE2P$q&+h2-9?_w{Js#!vu2(+o^yg!XnrxKWD4eb& zlt0dh1E|E^#}Za>t_miC1uEqJDn*GxAXOm;9YRQ{5%Q@O-omVcjxJczEFD~5L_3zv zWLCzm7GA2$%O#dedAZDTIWObI&uUu9%T<=EzHE<512=eaY?k45OyE_9jB%1LOphFB zRNxG)Q6-=rGvoa(00i4=*uSf5Adf{4g60MTfdG#>p0DB300?N4;{*atHUx7GEW8dt zN2?sVPry_7oV~2YLIUH+*%v|Jkc^8R;r_YK%A*6-5C|cKs^ZmFm@Fg_;Gc0~k_RXu zD!H8_xvlV8J6>Y5iRhB$kV}rPk&Et;3;B`3LoT{UF1kl9 zI+qLI9TeP(a#7Ly16_VTY6>1W6~&%QJ+N$`ZZ0Rjpp@8dB?L6a-9eY!9jURc&_8zv z&2)FLC4j&SlCH9= zNV{=vatBO~sd1C=-5^oDH{ zX~VCx%dzP5S^;7Wi)JeE0!aU~xJhY4p7 z?jD_(J~T5XG|nn@y=&iw>k#17-D?K@nBeW$p%GTPkNXJ#UsM`*7iJ};3O@}XwQT37 zZ@PV(`^f+)7yGax`xyb;XMJwNPf(2B+M2ypx&+_HC6U|w%l_zqE%)M*{}MqyW+-^@ z*9p4u<_O(CSCIE9@plWrf6kxckMmFo{HN!44Wybd{FK=jOvWepPWKorcaPKkdkD6Z zd}AE@a|=@PysPmC7x)tjB7O_#+uUy{biuo!FqZtzf-f$Uq5u5_570|*{<91@kIc+| zf>%%DE57>H*7S6HYTN#HW=L$L^7_vUw|6EEwZ?9Dpx5Ac9k#Y_x2;9EvRJ+EWv>a3rRJ_BsL+#FG7%d^{v?z z2Nc)|e0x1&#Y%kj^jEA)B$(*g_izI~W;*Njwd$N^5iUT1dIK2t6&dzr#G%1`%HnCI zu+kjQ+BeJ{bk_HL<}K<%FHWT0^QU6Hx**p}w}`ODaU#GJ1-}x^AYi(~ z1Q=(>6570uE+2AS9|D6-JOt(AUCeF#*;`sgS z)0#WjX>;U!?&liY>57$mHiir#fuu_Y-FOMz9)}VmlbtuyJKt8_GQMv8`pfV;ID%K^ z%YA#ey)$!YV!TcBMP1guY;K{CqbR}6_wSv1GunOIN^=+6`*ya+_5=03m!T6re*VO0}Wn-N=nsXf=iJ@t>Dip@ym-rIa%mY+zqjKjMyOIMbU9ThSV!g@Le zyE7OzW#b{n`+O#=_x)H=vtusv^)Y7BuLpnMW+bJvyz@Ss z@WmL$gpr?BoFkn=+B$ywmctXX;y6(6#}XW`nQ6~X&q*`;c%HvA%WX vl^fPyx#zMgHeEh;<+=@f_H5eNZe6i{&!$c5F5kUrY-D77?2577Fqi)Z6X04R literal 0 HcmV?d00001 From 3b184f4a45da71010fe1e260943a020e4767cba5 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 14:02:25 -0300 Subject: [PATCH 28/90] fix: Refactor deno.json to streamline testing tasks and update dependency versions for improved compatibility --- deno.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/deno.json b/deno.json index 1df652d..96ed3b1 100644 --- a/deno.json +++ b/deno.json @@ -4,20 +4,19 @@ "description": "A privacy-focused toolkit for the Moonlight protocol on Stellar Soroban smart contracts.", "license": "MIT", "tasks": { - "start": "deno run --allow-all index.ts", - "test-setup": "deno run --allow-all ./test/scripts/full-setup.ts", - "test": "deno test --allow-all", - "test:unit": "deno test --allow-all src/", - "test:integration": "deno test --allow-all test/integration/", - "test:coverage": "rm -rf coverage && deno test --allow-all --coverage=coverage", - "coverage:report": "deno coverage coverage", - "coverage:lcov": "deno coverage coverage --lcov > lcov.info", - "build": "deno bundle mod.ts dist/moonlight-sdk.js" + "test": "deno test -A --parallel --clean --ignore='_*/' --coverage=coverage && deno coverage coverage && deno task coverage:report", + "test:unit": "deno test -A --parallel --clean --ignore='_*/,**/*.integration.test.ts' --coverage=coverage && deno coverage coverage && deno task coverage:report", + "test:integration": "deno test -A --parallel --clean --ignore='_*/,**/*.unit.test.ts' --coverage=coverage && deno coverage coverage && deno task coverage:report", + "test:file": "deno test -A --parallel --clean --ignore='_*/,**/*.unit.test.ts'", + "coverage:report": "deno coverage --html" }, "nodeModulesDir": "auto", "exports": "./mod.ts", "imports": { "@fifo/convee": "jsr:@fifo/convee@^0.5.0", + "@colibri/core": "jsr:@colibri/core@^0.4.1", + "@stellar/stellar-sdk": "npm:@stellar/stellar-sdk@^14.0.0", + "@noble/curves": "jsr:@noble/curves", "@noble/hashes": "jsr:@noble/hashes", "@noble/secp256k1": "jsr:@noble/secp256k1", @@ -25,8 +24,9 @@ "jsr:@noble/hashes/sha256": "jsr:@noble/hashes/sha256@latest", "jsr:@noble/hashes/hkdf": "jsr:@noble/hashes/hkdf@latest", "jsr:@noble/curves/abstract/modular": "jsr:@noble/curves/abstract/modular@latest", - "@colibri/core": "jsr:@colibri/core@^0.4.0", - "@stellar/stellar-sdk": "npm:@stellar/stellar-sdk@^14.0.0", + + "@std/assert": "jsr:@std/assert@^1.0.0", + "@std/testing": "jsr:@std/testing@^1.0.0", "tslib": "npm:tslib@2.5.0", "buffer": "npm:buffer@6.0.3", "asn1js": "npm:asn1js@3.0.5" From 4edcc3a7d6d38608eb76ddbdaf5abe3a1f113b0b Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 14:02:31 -0300 Subject: [PATCH 29/90] feat: Add integration tests for AuthChannel functionality to validate client initialization, contract interaction, and provider management --- .../channel-auth.integration.test.ts | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 test/integration/channel-auth.integration.test.ts diff --git a/test/integration/channel-auth.integration.test.ts b/test/integration/channel-auth.integration.test.ts new file mode 100644 index 0000000..eeb66dd --- /dev/null +++ b/test/integration/channel-auth.integration.test.ts @@ -0,0 +1,153 @@ +import { assertEquals, assertExists } from "@std/assert"; +import { beforeAll, describe, it } from "@std/testing/bdd"; + +import { + LocalSigner, + NativeAccount, + TestNet, + initializeWithFriendbot, + Contract, +} from "@colibri/core"; + +import type { + Ed25519PublicKey, + TransactionConfig, + ContractId, +} from "@colibri/core"; +import { + AuthInvokeMethods, + AuthReadMethods, + AuthSpec, +} from "../../src/channel-auth/constants.ts"; +import type { Buffer } from "node:buffer"; +import { loadContractWasm } from "../helpers/load-wasm.ts"; +import { ChannelAuth } from "../../src/channel-auth/index.ts"; +import type { ChannelAuthConstructorArgs } from "../../src/channel-auth/types.ts"; + +describe("[Testnet - Integration] AuthChannel", () => { + const networkConfig = TestNet(); + + const admin = NativeAccount.fromMasterSigner(LocalSigner.generateRandom()); + const providerA = NativeAccount.fromMasterSigner( + LocalSigner.generateRandom() + ); + + const txConfig: TransactionConfig = { + fee: "100", + timeout: 30, + source: admin.address(), + signers: [admin.signer()], + }; + + let authWasm: Buffer; + + beforeAll(async () => { + await initializeWithFriendbot( + networkConfig.friendbotUrl, + admin.address() as Ed25519PublicKey + ); + + await initializeWithFriendbot( + networkConfig.friendbotUrl, + providerA.address() as Ed25519PublicKey + ); + + authWasm = loadContractWasm("channel_auth_contract"); + }); + + describe("Basic tests", () => { + let authId: ContractId; + + beforeAll(async () => { + const authContract = Contract.create({ + networkConfig, + contractConfig: { + spec: AuthSpec, + wasm: authWasm, + }, + }); + + await authContract.uploadWasm({ + ...txConfig, + }); + + await authContract.deploy({ + config: txConfig, + constructorArgs: { + admin: admin.address() as Ed25519PublicKey, + } as ChannelAuthConstructorArgs, + }); + + authId = authContract.getContractId(); + }); + + it("should initialize a client", () => { + const authClient = new ChannelAuth(networkConfig, authId); + + assertExists(authClient); + assertExists(authClient.getAuthId()); + assertEquals(authClient.getAuthId(), authId); + assertExists(authClient.getNetworkConfig()); + assertEquals(authClient.getNetworkConfig(), networkConfig); + }); + + it("should read from the contract and return the output", async () => { + const authClient = new ChannelAuth(networkConfig, authId); + + const adminAddress = await authClient.read({ + method: AuthReadMethods.admin, + methodArgs: {}, + }); + + const isProvider = await authClient.read({ + method: AuthReadMethods.is_provider, + methodArgs: { provider: admin.address() as Ed25519PublicKey }, + }); + + assertExists(adminAddress); + assertEquals(adminAddress, admin.address() as Ed25519PublicKey); + assertExists(isProvider); + assertEquals(isProvider, false); + }); + + it("should invoke the contract", async () => { + const authClient = new ChannelAuth(networkConfig, authId); + + let isProvider = await authClient.read({ + method: AuthReadMethods.is_provider, + methodArgs: { provider: providerA.address() as Ed25519PublicKey }, + }); + + assertExists(isProvider); + assertEquals(isProvider, false); + + await authClient.invoke({ + method: AuthInvokeMethods.add_provider, + methodArgs: { provider: providerA.address() as Ed25519PublicKey }, + config: txConfig, + }); + + isProvider = await authClient.read({ + method: AuthReadMethods.is_provider, + methodArgs: { provider: providerA.address() as Ed25519PublicKey }, + }); + + assertExists(isProvider); + assertEquals(isProvider, true); + + await authClient.invoke({ + method: AuthInvokeMethods.remove_provider, + methodArgs: { provider: providerA.address() as Ed25519PublicKey }, + config: txConfig, + }); + + isProvider = await authClient.read({ + method: AuthReadMethods.is_provider, + methodArgs: { provider: providerA.address() as Ed25519PublicKey }, + }); + + assertExists(isProvider); + assertEquals(isProvider, false); + }); + }); +}); From 04f7e9f5d74ca66638ad912fb180e62245960ec3 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 14:03:01 -0300 Subject: [PATCH 30/90] fix: Update transaction fee in AuthChannel integration test for accurate testing --- test/integration/channel-auth.integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/channel-auth.integration.test.ts b/test/integration/channel-auth.integration.test.ts index eeb66dd..2631124 100644 --- a/test/integration/channel-auth.integration.test.ts +++ b/test/integration/channel-auth.integration.test.ts @@ -33,7 +33,7 @@ describe("[Testnet - Integration] AuthChannel", () => { ); const txConfig: TransactionConfig = { - fee: "100", + fee: "1000000", timeout: 30, source: admin.address(), signers: [admin.signer()], From 59942f2e24ef5591e9220cb279b1e0199bc6586c Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 14:04:00 -0300 Subject: [PATCH 31/90] fix: Correct test suite description for ChannelAuth integration tests to ensure accurate identification and context --- test/integration/channel-auth.integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/channel-auth.integration.test.ts b/test/integration/channel-auth.integration.test.ts index 2631124..96a6e94 100644 --- a/test/integration/channel-auth.integration.test.ts +++ b/test/integration/channel-auth.integration.test.ts @@ -24,7 +24,7 @@ import { loadContractWasm } from "../helpers/load-wasm.ts"; import { ChannelAuth } from "../../src/channel-auth/index.ts"; import type { ChannelAuthConstructorArgs } from "../../src/channel-auth/types.ts"; -describe("[Testnet - Integration] AuthChannel", () => { +describe("[Testnet - Integration] ChannelAuth", () => { const networkConfig = TestNet(); const admin = NativeAccount.fromMasterSigner(LocalSigner.generateRandom()); From f973ae451d6987031d3f2cdb453a7e57a4026652 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 14:17:26 -0300 Subject: [PATCH 32/90] refactor: Remove pool-engine integration test file to streamline test suite --- .../pool-engine.integration.test.ts | 377 ------------------ 1 file changed, 377 deletions(-) delete mode 100644 test/integration/pool-engine.integration.test.ts diff --git a/test/integration/pool-engine.integration.test.ts b/test/integration/pool-engine.integration.test.ts deleted file mode 100644 index 840e059..0000000 --- a/test/integration/pool-engine.integration.test.ts +++ /dev/null @@ -1,377 +0,0 @@ -// filepath: /Users/fifo/Documents/moonlight/moonlight-sdk/test/integration/pool-engine.integration.test.ts -import { - assertEquals, - assertExists, -} from "https://deno.land/std@0.207.0/assert/mod.ts"; -import { StellarPlus } from "stellar-plus"; -import { - PoolEngine, - ReadMethods, - WriteMethods, - assembleNetworkContext, - StellarDerivator, - type StellarNetworkId, - type StellarSmartContractId, - UTXOKeypairBase, -} from "../../mod.ts"; -import { loadContractWasm } from "../helpers/load-wasm.ts"; -import type { TransactionInvocation } from "stellar-plus/lib/stellar-plus/types"; -import { Buffer } from "buffer"; -import type { SorobanTransactionPipelineOutputVerbose } from "stellar-plus/lib/stellar-plus/core/pipelines/soroban-transaction/types"; - -const { DefaultAccountHandler } = StellarPlus.Account; - -const contractFileName = "privacy_pool"; - -const XLM_CONTRACT_ID_TESTNET = - "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; - -Deno.test("PoolEngine Integration - Deposit and Withdraw", async (t) => { - // Test environment setup - const networkConfig = StellarPlus.Network.TestNet(); - - const admin = new DefaultAccountHandler({ - networkConfig: networkConfig, - }); - - const txInvocation: TransactionInvocation = { - header: { - source: admin.getPublicKey(), - fee: "10000000", // 1 XLM base fee - timeout: 30, - }, - signers: [admin], - }; - - const wasmBinary = await loadContractWasm(contractFileName); - - let poolEngine: PoolEngine; - let utxo: Uint8Array; - let utxoKeypair: UTXOKeypairBase; - const depositAmount = 1000000n; // 0.1 XLM - - let derivator: StellarDerivator; - - // Setup before all tests - await t.step( - "setup: initialize test environment prerequisites and pool instance", - async () => { - await admin.initializeWithFriendbot(); - - poolEngine = await PoolEngine.create({ - networkConfig, - wasm: wasmBinary, - assetContractId: XLM_CONTRACT_ID_TESTNET, - }); - - assertExists(poolEngine, "Pool engine should be initialized"); - } - ); - - // upload wasm - await t.step("setup: upload wasm", async () => { - await poolEngine.uploadWasm(txInvocation); - - //vassert getWasmHash returns the wasmhash and doesnt throw - const wasmHash = poolEngine.getWasmHash(); - assertExists(wasmHash, "Wasm hash should be generated"); - }); - - // deploy new instance of contract - await t.step("setup: deploy contract", async () => { - await poolEngine.deploy({ - ...txInvocation, - contractArgs: { admin: admin.getPublicKey() }, - }); - - //assert contract id existis and is of type string with C prefix - const contractId = poolEngine.getContractId(); - assertExists(contractId, "Contract ID should be generated"); - assertEquals( - contractId.startsWith("C"), - true, - "Contract ID should start with C" - ); - }); - - //post setup, admin should be the admin provided during construction - await t.step("setup: check admin", async () => { - const adminResult = await poolEngine.read({ - ...txInvocation, - method: ReadMethods.admin, - methodArgs: {}, - }); - - assertEquals( - adminResult, - admin.getPublicKey(), - "Admin should match the provided admin" - ); - }); - - await t.step("read: supply should be 0 at initialization", async () => { - const supplyResult = await poolEngine.read({ - ...txInvocation, - method: ReadMethods.supply, - methodArgs: {}, - }); - - assertExists(supplyResult, "Supply result should exist"); - assertEquals(supplyResult, 0n, "Supply should be 0 at initialization"); - }); - - // testcase: pool should return its derivator - await t.step("pool should return its derivator", async () => { - // A PoolEngine instance should have a derivator property that helps - // with deriving UTXOs and related cryptographic material - derivator = poolEngine.derivator; - - assertExists(derivator, "Pool engine should have a derivator"); - assertEquals( - derivator instanceof StellarDerivator, - true, - "Derivator should be an instance of StellarDerivator" - ); - - // Verify the derivator has been initialized with the correct network and contract - assertEquals( - derivator.getContext(), - assembleNetworkContext( - networkConfig.networkPassphrase as StellarNetworkId, - poolEngine.getContractId() as StellarSmartContractId - ), - "Derivator should be initialized with the correct network" - ); - }); - - await t.step( - "setup: attach root to derivator and generate plain seed correctly", - async () => { - derivator.withRoot("S-MOCKED_SECRET_ROOT"); - const seed = derivator.assembleSeed("1"); - assertExists(seed, "Derivation seed should be generated"); - assertEquals( - seed.includes("S-MOCKED_SECRET_ROOT"), - true, - "Seed should contain the secret root" - ); - assertEquals(seed.includes("1"), true, "Seed should contain the index"); - assertEquals( - seed.includes(poolEngine.derivator.getContext()), - true, - "Seed should contain the context" - ); - } - ); - - // testcase, setup and generate the UTXO using the derivator, any test root as seed and the utxobase keypair - await t.step("generate UTXO using the derivator with test seed", async () => { - // Generate a keypair using the derivator with the established root - utxoKeypair = new UTXOKeypairBase(await derivator.deriveKeypair("1")); - - assertExists(utxoKeypair, "UTXO keypair should be generated"); - assertExists(utxoKeypair.publicKey, "Public key should be generated"); - assertExists(utxoKeypair.privateKey, "Private key should be generated"); - - utxo = utxoKeypair.publicKey; - - assertEquals(utxo.byteLength, 65, "Public key should be 65 bytes"); - - // Verify the keypair can sign data - const testData = new Uint8Array(32); - crypto.getRandomValues(testData); - - const signature = await utxoKeypair.signPayload(testData); - assertExists(signature, "Should be able to generate a signature"); - }); - - await t.step( - "read: should correctly return the balance of the utxo before creating it", - async () => { - // Verify the balance of the UTXO after deposit - const balanceResult = await poolEngine.read({ - ...txInvocation, - method: ReadMethods.balance, - methodArgs: { - utxo: Buffer.from(utxo), - }, - }); - - assertEquals( - balanceResult, - -1n, - "UTXO balance should be -1 before creating it" - ); - } - ); - - // Perform a deposit to the pool - await t.step( - "deposit: should successfully deposit tokens into the pool", - async () => { - const depositInvocation = { - method: WriteMethods.deposit, - methodArgs: { - from: admin.getPublicKey(), - amount: depositAmount, - utxo: Buffer.from(utxo), - }, - }; - - // Execute the deposit transaction - const depositResult = (await poolEngine.write({ - ...depositInvocation, - ...txInvocation, - options: { verboseOutput: true, includeHashOutput: true }, - })) as SorobanTransactionPipelineOutputVerbose; - - // Verify transaction was successful - assertExists( - depositResult.sorobanTransactionOutput, - "Deposit transaction result should exist" - ); - assertExists( - depositResult.hash, - "Deposit transaction should be successful" - ); - } - ); - - await t.step( - "read: should correctly return the balance of the utxo after creating it", - async () => { - // Verify the balance of the UTXO after deposit - const balanceResult = await poolEngine.read({ - ...txInvocation, - method: ReadMethods.balance, - methodArgs: { - utxo: Buffer.from(utxo), - }, - }); - - assertEquals( - balanceResult, - depositAmount, - "UTXO balance should match the deposited amount" - ); - } - ); - - await t.step( - "read: should correctly return the total supply after depositing", - async () => { - const supplyResult = await poolEngine.read({ - ...txInvocation, - method: ReadMethods.supply, - methodArgs: {}, - }); - - assertExists(supplyResult, "Supply result should exist"); - assertEquals( - supplyResult, - depositAmount, - "Total supply should increase by the deposited amount" - ); - } - ); - - // Withdraw from the pool - await t.step( - "withdraw: should successfully withdraw tokens from the pool", - async () => { - // Generate a withdraw payload using the pool engine's helper - const withdrawPayload = poolEngine.buildWithdrawPayload({ - utxo: utxo, - amount: depositAmount, - }); - - // Sign the withdraw payload using the UTXO keypair - const signature = await utxoKeypair.signPayload(withdrawPayload); - assertExists(signature, "Should generate a valid signature"); - - const withdrawInvocation = { - method: WriteMethods.withdraw, - methodArgs: { - to: admin.getPublicKey(), - amount: depositAmount, - utxo: Buffer.from(utxo), - signature: Buffer.from(signature), - }, - }; - - try { - // Execute the withdrawal transaction - const withdrawResult = (await poolEngine.write({ - ...withdrawInvocation, - ...txInvocation, - options: { verboseOutput: true, includeHashOutput: true }, - })) as SorobanTransactionPipelineOutputVerbose; - - assertExists( - withdrawResult.sorobanTransactionOutput, - "Withdraw transaction result should exist" - ); - assertExists( - withdrawResult.hash, - "Withdraw transaction should be successful" - ); - - // Verify the balance of the UTXO after withdrawal - const balanceResult = await poolEngine.read({ - ...txInvocation, - method: ReadMethods.balance, - methodArgs: { - utxo: Buffer.from(utxo), - }, - }); - - assertEquals( - balanceResult, - 0n, - "UTXO balance should be zero after withdrawal" - ); - } catch (error) { - console.log("Error during withdrawal:", error); - throw error; // Re-throw to fail the test if withdrawal fails - } - } - ); - - await t.step( - "read: should correctly return the balance of the utxo after spending", - async () => { - // Verify the balance of the UTXO after deposit - const balanceResult = await poolEngine.read({ - ...txInvocation, - method: ReadMethods.balance, - methodArgs: { - utxo: Buffer.from(utxo), - }, - }); - - assertEquals( - balanceResult, - 0n, - "UTXO balance should be 0 after spending it" - ); - } - ); - - await t.step( - "read: should correctly return the total supply after withdrawing", - async () => { - const supplyResult = await poolEngine.read({ - ...txInvocation, - method: ReadMethods.supply, - methodArgs: {}, - }); - - assertExists(supplyResult, "Supply result should exist"); - assertEquals( - supplyResult, - 0n, - "Total supply should decrease by the withdrawn amount" - ); - } - ); -}); From 47a07b11027bc694d7edecb0a895d6f5c83d45fd Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 14:17:33 -0300 Subject: [PATCH 33/90] feat: Add disableSanitizeConfig to manage sanitization settings for testing --- test/utils/disable-sanitize-config.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 test/utils/disable-sanitize-config.ts diff --git a/test/utils/disable-sanitize-config.ts b/test/utils/disable-sanitize-config.ts new file mode 100644 index 0000000..1670b02 --- /dev/null +++ b/test/utils/disable-sanitize-config.ts @@ -0,0 +1,5 @@ +export const disableSanitizeConfig = { + sanitizeResources: false, + sanitizeExit: false, + sanitizeOps: false, +}; From 2e415b0dbf3e3c00696a900d2d6ec9db4d3e2fc3 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 14:17:41 -0300 Subject: [PATCH 34/90] feat: Add assetId property and getter method to PrivacyChannel class for enhanced asset management --- src/privacy-channel/index.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index 43c3ea1..1b53592 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -16,13 +16,15 @@ import type { ChannelInvoke, ChannelRead } from "./types.ts"; export class PrivacyChannel { private _client: Contract; private _authId: ContractId; + private _assetId: ContractId; private _networkConfig: NetworkConfig; private _derivator: StellarDerivator; public constructor( networkConfig: NetworkConfig, channelId: ContractId, - authId: ContractId + authId: ContractId, + assetId: ContractId ) { this._networkConfig = networkConfig; @@ -33,6 +35,8 @@ export class PrivacyChannel { this._authId = authId; + this._assetId = assetId; + this._derivator = new StellarDerivator().withNetworkAndContract( networkConfig.networkPassphrase as StellarNetworkId, channelId as ContractId @@ -56,8 +60,9 @@ export class PrivacyChannel { private require(arg: "_authId"): ContractId; private require(arg: "_networkConfig"): NetworkConfig; private require(arg: "_derivator"): StellarDerivator; + private require(arg: "_assetId"): ContractId; private require( - arg: "_client" | "_authId" | "_networkConfig" | "_derivator" + arg: "_client" | "_authId" | "_networkConfig" | "_derivator" | "_assetId" ): Contract | ContractId | NetworkConfig | StellarDerivator { if (this[arg]) return this[arg]; throw new Error(`Property ${arg} is not set in the Channel instance.`); @@ -91,6 +96,17 @@ export class PrivacyChannel { return this.require("_authId"); } + /** + * Returns the Asset contract ID. + * + * @params None + * @returns {ContractId} The Asset contract ID. + * @throws {Error} If the Asset contract ID is not set. + * */ + public getAssetId(): ContractId { + return this.require("_assetId"); + } + /** * Returns the NetworkConfig instance. * From 3faa48b5a48633d232bb760a97aa9dd4906aefc4 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 14:17:47 -0300 Subject: [PATCH 35/90] fix: Add disableSanitizeConfig to ChannelAuth integration tests for improved test accuracy --- test/integration/channel-auth.integration.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/integration/channel-auth.integration.test.ts b/test/integration/channel-auth.integration.test.ts index 96a6e94..71e63c7 100644 --- a/test/integration/channel-auth.integration.test.ts +++ b/test/integration/channel-auth.integration.test.ts @@ -23,8 +23,9 @@ import type { Buffer } from "node:buffer"; import { loadContractWasm } from "../helpers/load-wasm.ts"; import { ChannelAuth } from "../../src/channel-auth/index.ts"; import type { ChannelAuthConstructorArgs } from "../../src/channel-auth/types.ts"; +import { disableSanitizeConfig } from "../utils/disable-sanitize-config.ts"; -describe("[Testnet - Integration] ChannelAuth", () => { +describe("[Testnet - Integration] ChannelAuth", disableSanitizeConfig, () => { const networkConfig = TestNet(); const admin = NativeAccount.fromMasterSigner(LocalSigner.generateRandom()); From bd4cc1d79c5088c1b572257968da7813a7ca59e4 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 16 Oct 2025 14:18:54 -0300 Subject: [PATCH 36/90] feat: Implement PrivacyChannel integration tests for enhanced contract interaction validation - Added comprehensive integration tests for the PrivacyChannel, ensuring proper initialization and contract interactions. - Included tests for reading various contract methods and validating expected outputs. - Utilized disableSanitizeConfig for accurate testing in the testnet environment. --- .../privacy-channel.integration.test.ts | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 test/integration/privacy-channel.integration.test.ts diff --git a/test/integration/privacy-channel.integration.test.ts b/test/integration/privacy-channel.integration.test.ts new file mode 100644 index 0000000..0bb057a --- /dev/null +++ b/test/integration/privacy-channel.integration.test.ts @@ -0,0 +1,198 @@ +import { assertEquals, assertExists } from "@std/assert"; +import { beforeAll, describe, it } from "@std/testing/bdd"; + +import { + LocalSigner, + NativeAccount, + TestNet, + initializeWithFriendbot, + Contract, +} from "@colibri/core"; + +import type { + Ed25519PublicKey, + TransactionConfig, + ContractId, +} from "@colibri/core"; +import { AuthSpec } from "../../src/channel-auth/constants.ts"; +import type { Buffer } from "node:buffer"; +import { loadContractWasm } from "../helpers/load-wasm.ts"; +import type { ChannelAuthConstructorArgs } from "../../src/channel-auth/types.ts"; +import type { ChannelConstructorArgs } from "../../src/privacy-channel/types.ts"; +import { + ChannelReadMethods, + ChannelSpec, +} from "../../src/privacy-channel/constants.ts"; +import { Asset } from "@stellar/stellar-sdk"; +import { PrivacyChannel } from "../../src/privacy-channel/index.ts"; +import { disableSanitizeConfig } from "../utils/disable-sanitize-config.ts"; +import { generateP256KeyPair } from "../../src/utils/secp256r1/generateP256KeyPair.ts"; + +describe( + "[Testnet - Integration] PrivacyChannel", + disableSanitizeConfig, + () => { + const networkConfig = TestNet(); + + const admin = NativeAccount.fromMasterSigner(LocalSigner.generateRandom()); + const providerA = NativeAccount.fromMasterSigner( + LocalSigner.generateRandom() + ); + + const txConfig: TransactionConfig = { + fee: "1000000", + timeout: 30, + source: admin.address(), + signers: [admin.signer()], + }; + + const assetId = Asset.native().contractId( + networkConfig.networkPassphrase + ) as ContractId; + + let authWasm: Buffer; + let channelWasm: Buffer; + let authId: ContractId; + let channelId: ContractId; + + beforeAll(async () => { + await initializeWithFriendbot( + networkConfig.friendbotUrl, + admin.address() as Ed25519PublicKey + ); + + await initializeWithFriendbot( + networkConfig.friendbotUrl, + providerA.address() as Ed25519PublicKey + ); + + authWasm = loadContractWasm("channel_auth_contract"); + channelWasm = loadContractWasm("privacy_channel"); + + const authContract = Contract.create({ + networkConfig, + contractConfig: { + spec: AuthSpec, + wasm: authWasm, + }, + }); + + await authContract.uploadWasm({ + ...txConfig, + }); + + await authContract.deploy({ + config: txConfig, + constructorArgs: { + admin: admin.address() as Ed25519PublicKey, + } as ChannelAuthConstructorArgs, + }); + + authId = authContract.getContractId(); + }); + + describe("Basic tests", () => { + beforeAll(async () => { + const channelContract = Contract.create({ + networkConfig, + contractConfig: { + spec: ChannelSpec, + wasm: channelWasm, + }, + }); + + await channelContract.uploadWasm({ + ...txConfig, + }); + + await channelContract.deploy({ + config: txConfig, + constructorArgs: { + admin: admin.address() as Ed25519PublicKey, + auth_contract: authId, + asset: assetId, + } as ChannelConstructorArgs, + }); + + channelId = channelContract.getContractId(); + }); + + it("should initialize a client", () => { + const channelClient = new PrivacyChannel( + networkConfig, + channelId, + authId, + assetId + ); + + assertExists(channelClient); + assertExists(channelClient.getAuthId()); + assertEquals(channelClient.getAuthId(), authId); + assertExists(channelClient.getNetworkConfig()); + assertEquals(channelClient.getNetworkConfig(), networkConfig); + assertExists(channelClient.getAssetId()); + assertEquals(channelClient.getAssetId(), assetId); + assertExists(channelClient.getChannelId()); + assertEquals(channelClient.getChannelId(), channelId); + assertExists(channelClient.getDerivator()); + }); + + it("should read from the contract and return the output", async () => { + const channelClient = new PrivacyChannel( + networkConfig, + channelId, + authId, + assetId + ); + + const utxoKeypair = generateP256KeyPair(); + + const adminAddress = await channelClient.read({ + method: ChannelReadMethods.admin, + methodArgs: {}, + }); + + const asset = await channelClient.read({ + method: ChannelReadMethods.asset, + methodArgs: {}, + }); + + const auth = await channelClient.read({ + method: ChannelReadMethods.auth, + methodArgs: {}, + }); + + const supply = await channelClient.read({ + method: ChannelReadMethods.supply, + methodArgs: {}, + }); + + const utxoBal = await channelClient.read({ + method: ChannelReadMethods.utxo_balance, + methodArgs: { utxo: (await utxoKeypair).publicKey as Buffer }, + }); + + assertExists(adminAddress); + assertEquals(adminAddress, admin.address() as Ed25519PublicKey); + assertExists(asset); + assertEquals(asset, assetId); + assertExists(auth); + assertEquals(auth, authId); + assertExists(supply); + assertEquals(supply, 0n); + assertExists(utxoBal); + assertEquals(utxoBal, -1n); // UTXO is in Unused state + }); + + //TODO: Complete this test once we have the tx builder + it.skip("should invoke the contract", async () => { + // const channelClient = new PrivacyChannel( + // networkConfig, + // channelId, + // authId, + // assetId + // ); + }); + }); + } +); From 63d578af2fb0467b8402fedc09722dbc88252f80 Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Wed, 15 Oct 2025 17:58:25 -0300 Subject: [PATCH 37/90] refactor: Change the transaction builder context structure to be more maintainable and contextualized by responsibilities --- src/core/config/index.ts | 21 --- .../auth/bundle-auth-entry.ts | 15 ++ .../auth/deposit-auth-entry.ts | 16 ++ src/transaction-builder/auth/index.ts | 3 + src/transaction-builder/auth/payload-hash.ts | 28 +++ src/transaction-builder/index.ts | 175 ++++-------------- src/transaction-builder/index.unit.test.ts | 116 ++++++------ src/transaction-builder/signatures/index.ts | 1 + .../signatures/signatures-xdr.ts | 64 +++++++ src/transaction-builder/utils/index.ts | 1 + src/transaction-builder/utils/ordering.ts | 10 + src/transaction-builder/validators/index.ts | 1 + .../validators/operations.ts | 42 +++++ src/transaction-builder/xdr/index.ts | 1 + src/transaction-builder/xdr/ops-to-xdr.ts | 47 +++++ src/utils/auth/build-auth-payload.ts | 19 +- src/utils/auth/bundle-auth-entry.ts | 5 +- src/utils/common/index.ts | 18 -- 18 files changed, 327 insertions(+), 256 deletions(-) delete mode 100644 src/core/config/index.ts create mode 100644 src/transaction-builder/auth/bundle-auth-entry.ts create mode 100644 src/transaction-builder/auth/deposit-auth-entry.ts create mode 100644 src/transaction-builder/auth/index.ts create mode 100644 src/transaction-builder/auth/payload-hash.ts create mode 100644 src/transaction-builder/signatures/index.ts create mode 100644 src/transaction-builder/signatures/signatures-xdr.ts create mode 100644 src/transaction-builder/utils/index.ts create mode 100644 src/transaction-builder/utils/ordering.ts create mode 100644 src/transaction-builder/validators/index.ts create mode 100644 src/transaction-builder/validators/operations.ts create mode 100644 src/transaction-builder/xdr/index.ts create mode 100644 src/transaction-builder/xdr/ops-to-xdr.ts diff --git a/src/core/config/index.ts b/src/core/config/index.ts deleted file mode 100644 index 93a7c92..0000000 --- a/src/core/config/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Server } from "@stellar/stellar-sdk/rpc"; -import { highlightText } from "../../utils/common" - -export function getRequiredEnv(key: string): string { - const value = Deno.env.get(key); - if (!value) { - console.error( - highlightText( - `Error: Environment variable ${key} is not set.\nCheck the 'Setup' section of the README.md file.`, - "red" - ) - ); - - throw new Error(`Required environment variable ${key} is not set. `); - } - return value; -} - -export const getRpc = () => { - return new Server(getRequiredEnv("STELLAR_RPC_URL"), { allowHttp: true }); -}; \ No newline at end of file diff --git a/src/transaction-builder/auth/bundle-auth-entry.ts b/src/transaction-builder/auth/bundle-auth-entry.ts new file mode 100644 index 0000000..304662a --- /dev/null +++ b/src/transaction-builder/auth/bundle-auth-entry.ts @@ -0,0 +1,15 @@ +import { xdr } from "@stellar/stellar-sdk"; +import { generateBundleAuthEntry } from "../../utils/auth/bundle-auth-entry.ts"; + +export const buildBundleAuthEntry = (args: { + channelId: string; + authId: string; + args: xdr.ScVal[]; + nonce: string; + signatureExpirationLedger: number; + signaturesXdr?: string; +}): xdr.SorobanAuthorizationEntry => { + return generateBundleAuthEntry(args); +}; + + diff --git a/src/transaction-builder/auth/deposit-auth-entry.ts b/src/transaction-builder/auth/deposit-auth-entry.ts new file mode 100644 index 0000000..1c371bd --- /dev/null +++ b/src/transaction-builder/auth/deposit-auth-entry.ts @@ -0,0 +1,16 @@ +import { xdr } from "@stellar/stellar-sdk"; +import { generateDepositAuthEntry } from "../../utils/auth/deposit-auth-entry.ts"; + +export const buildDepositAuthEntry = (args: { + channelId: string; + assetId: string; + depositor: string; + amount: bigint; + conditions: xdr.ScVal[]; + nonce: string; + signatureExpirationLedger: number; +}): xdr.SorobanAuthorizationEntry => { + return generateDepositAuthEntry(args); +}; + + diff --git a/src/transaction-builder/auth/index.ts b/src/transaction-builder/auth/index.ts new file mode 100644 index 0000000..58976bb --- /dev/null +++ b/src/transaction-builder/auth/index.ts @@ -0,0 +1,3 @@ +export * from "./bundle-auth-entry.ts"; +export * from "./deposit-auth-entry.ts"; +export * from "./payload-hash.ts"; diff --git a/src/transaction-builder/auth/payload-hash.ts b/src/transaction-builder/auth/payload-hash.ts new file mode 100644 index 0000000..6085afa --- /dev/null +++ b/src/transaction-builder/auth/payload-hash.ts @@ -0,0 +1,28 @@ +import { xdr, hash } from "@stellar/stellar-sdk"; +import { Buffer } from "buffer"; +import { sha256Buffer } from "../../utils/hash/sha256Buffer.ts"; + +export const buildOperationAuthEntryHash = async (params: { + network: string; + rootInvocation: xdr.SorobanAuthorizedInvocation; + nonce: string; + signatureExpirationLedger: number; +}): Promise => { + const networkId = hash(Buffer.from(params.network)); + + const preImageInner = new xdr.HashIdPreimageSorobanAuthorization({ + networkId, + nonce: xdr.Int64.fromString(params.nonce), + signatureExpirationLedger: params.signatureExpirationLedger, + invocation: params.rootInvocation, + }); + + const preImage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization( + preImageInner, + ); + + const payload = preImage.toXDR(); + return Buffer.from(await sha256Buffer(payload)); +}; + + diff --git a/src/transaction-builder/index.ts b/src/transaction-builder/index.ts index ad09401..8524c30 100644 --- a/src/transaction-builder/index.ts +++ b/src/transaction-builder/index.ts @@ -1,59 +1,23 @@ import { Asset, authorizeEntry, - hash, Keypair, - StrKey, xdr, - nativeToScVal, } from "@stellar/stellar-sdk"; +import { Buffer } from "buffer"; import { CreateOperation, DepositOperation, SpendOperation, WithdrawOperation, UTXOPublicKey, Ed25519PublicKey } from "../transaction-builder/types.ts"; import { StellarSmartContractId } from "../utils/types/stellar.types.ts"; import { Condition } from "../conditions/types.ts"; -import { sha256Buffer } from "../utils/hash/sha256Buffer.ts"; import { generateNonce } from "../utils/common/index.ts"; -import { generateDepositAuthEntry } from "../utils/auth/deposit-auth-entry.ts"; -import { generateBundleAuthEntry } from "../utils/auth/bundle-auth-entry.ts"; import { conditionToXDR } from "../conditions/index.ts"; import { MoonlightOperation } from "../transaction-builder/types.ts"; import { buildAuthPayloadHash } from "../utils/auth/build-auth-payload.ts"; import { IUTXOKeypairBase } from "../core/utxo-keypair-base/types.ts"; - -export const createOpToXDR = (op: CreateOperation): xdr.ScVal => { - return xdr.ScVal.scvVec([ - xdr.ScVal.scvBytes(Buffer.from(op.utxo as Uint8Array)), - nativeToScVal(op.amount, { type: "i128" }), - ]); -}; - -export const depositOpToXDR = (op: DepositOperation): xdr.ScVal => { - return xdr.ScVal.scvVec([ - nativeToScVal(op.pubKey, { type: "address" }), - nativeToScVal(op.amount, { type: "i128" }), - op.conditions.length === 0 - ? xdr.ScVal.scvVec(null) - : xdr.ScVal.scvVec(op.conditions.map((c) => conditionToXDR(c))), - ]); -}; - -export const withdrawOpToXDR = (op: WithdrawOperation): xdr.ScVal => { - return xdr.ScVal.scvVec([ - nativeToScVal(op.pubKey, { type: "address" }), - nativeToScVal(op.amount, { type: "i128" }), - op.conditions.length === 0 - ? xdr.ScVal.scvVec(null) - : xdr.ScVal.scvVec(op.conditions.map((c) => conditionToXDR(c))), - ]); -}; - -export const spendOpToXDR = (op: SpendOperation): xdr.ScVal => { - return xdr.ScVal.scvVec([ - xdr.ScVal.scvBytes(Buffer.from(op.utxo as Uint8Array)), - op.conditions.length === 0 - ? xdr.ScVal.scvVec(null) - : xdr.ScVal.scvVec(op.conditions.map((c) => conditionToXDR(c))), - ]); -}; +import { createOpToXDR, depositOpToXDR, withdrawOpToXDR, spendOpToXDR } from "./xdr/index.ts"; +import { buildSignaturesXDR } from "./signatures/index.ts"; +import { buildBundleAuthEntry, buildDepositAuthEntry, buildOperationAuthEntryHash } from "./auth/index.ts"; +import { orderSpendByUtxo } from "./utils/index.ts"; +import { assertPositiveAmount, assertNoDuplicateCreate, assertNoDuplicateSpend, assertNoDuplicatePubKey, assertSpendExists } from "./validators/index.ts"; export class MoonlightTransactionBuilder { private create: CreateOperation[] = []; @@ -66,11 +30,11 @@ export class MoonlightTransactionBuilder { private network: string; private innerSignatures: Map< Uint8Array, - { sig: Buffer; exp: number } + { sig: Buffer; exp: number } > = new Map(); private providerInnerSignatures: Map< Ed25519PublicKey, - { sig: Buffer; exp: number; nonce: string } + { sig: Buffer; exp: number; nonce: string } > = new Map(); private extSignatures: Map = new Map(); @@ -93,19 +57,15 @@ export class MoonlightTransactionBuilder { } addCreate(utxo: UTXOPublicKey, amount: bigint) { - if (this.create.find((c) => Buffer.from(c.utxo).equals(Buffer.from(utxo)))) - throw new Error("Create operation for this UTXO already exists"); - - if (amount <= 0n) - throw new Error("Create operation amount must be positive"); + assertNoDuplicateCreate(this.create, utxo); + assertPositiveAmount(amount, "Create operation"); this.create.push({ utxo, amount }); return this; } addSpend(utxo: UTXOPublicKey, conditions: Condition[]) { - if (this.spend.find((s) => Buffer.from(s.utxo).equals(Buffer.from(utxo)))) - throw new Error("Spend operation for this UTXO already exists"); + assertNoDuplicateSpend(this.spend, utxo); this.spend.push({ utxo, conditions }); return this; @@ -116,11 +76,8 @@ export class MoonlightTransactionBuilder { amount: bigint, conditions: Condition[] ) { - if (this.deposit.find((d) => d.pubKey === pubKey)) - throw new Error("Deposit operation for this public key already exists"); - - if (amount <= 0n) - throw new Error("Deposit operation amount must be positive"); + assertNoDuplicatePubKey(this.deposit, pubKey, "Deposit"); + assertPositiveAmount(amount, "Deposit operation"); this.deposit.push({ pubKey, amount, conditions }); return this; @@ -131,11 +88,8 @@ export class MoonlightTransactionBuilder { amount: bigint, conditions: Condition[] ) { - if (this.withdraw.find((d) => d.pubKey === pubKey)) - throw new Error("Withdraw operation for this public key already exists"); - - if (amount <= 0n) - throw new Error("Withdraw operation amount must be positive"); + assertNoDuplicatePubKey(this.withdraw, pubKey, "Withdraw"); + assertPositiveAmount(amount, "Withdraw operation"); this.withdraw.push({ pubKey, amount, conditions }); return this; @@ -143,11 +97,10 @@ export class MoonlightTransactionBuilder { addInnerSignature( utxo: UTXOPublicKey, - signature: Buffer, + signature: Buffer, expirationLedger: number ) { - if (!this.spend.find((s) => Buffer.from(s.utxo).equals(Buffer.from(utxo)))) - throw new Error("No spend operation for this UTXO"); + assertSpendExists(this.spend, utxo); this.innerSignatures.set(utxo, { sig: signature, exp: expirationLedger }); return this; @@ -155,7 +108,7 @@ export class MoonlightTransactionBuilder { addProviderInnerSignature( pubKey: Ed25519PublicKey, - signature: Buffer, + signature: Buffer, expirationLedger: number, nonce: string ) { @@ -204,7 +157,7 @@ export class MoonlightTransactionBuilder { const deposit = this.getDepositOperation(address); if (!deposit) throw new Error("No deposit operation for this address"); - return generateDepositAuthEntry({ + return buildDepositAuthEntry({ channelId: this.channelId, assetId: this.asset.contractId(this.network), depositor: address, @@ -220,9 +173,7 @@ export class MoonlightTransactionBuilder { const signers: xdr.ScMapEntry[] = []; - const orderedSpend = this.spend.sort((a, b) => - Buffer.from(a.utxo).compare(Buffer.from(b.utxo)) - ); + const orderedSpend = orderSpendByUtxo(this.spend); for (const spend of orderedSpend) { signers.push( @@ -246,7 +197,7 @@ export class MoonlightTransactionBuilder { ): xdr.SorobanAuthorizationEntry { const reqArgs: xdr.ScVal[] = this.getAuthRequirementArgs(); - return generateBundleAuthEntry({ + return buildBundleAuthEntry({ channelId: this.channelId, authId: this.authId, args: reqArgs, @@ -267,7 +218,7 @@ export class MoonlightTransactionBuilder { const reqArgs: xdr.ScVal[] = this.getAuthRequirementArgs(); - return generateBundleAuthEntry({ + return buildBundleAuthEntry({ channelId: this.channelId, authId: this.authId, args: reqArgs, @@ -281,89 +232,29 @@ export class MoonlightTransactionBuilder { nonce: string, signatureExpirationLedger: number ): Promise { - const networkId = hash(Buffer.from(this.network)); - const rootInvocation = this.getOperationAuthEntry( nonce, signatureExpirationLedger ).rootInvocation(); - - const bundleHashPreImageInner = new xdr.HashIdPreimageSorobanAuthorization({ - networkId: networkId, - nonce: xdr.Int64.fromString(nonce), + return buildOperationAuthEntryHash({ + network: this.network, + rootInvocation, + nonce, signatureExpirationLedger, - invocation: rootInvocation, }); - - const bundleHashPreImage = - xdr.HashIdPreimage.envelopeTypeSorobanAuthorization( - bundleHashPreImageInner - ); - - const xdrPayload = bundleHashPreImage.toXDR(); - - // Get the XDR buffer and hash it - return Buffer.from(await sha256Buffer(xdrPayload)); } signaturesXDR(): string { const providerSigners = Array.from(this.providerInnerSignatures.keys()); - const spendSigners = Array.from(this.innerSignatures.keys()); - - const ortderedProviderSigners = providerSigners.sort((a, b) => - a.localeCompare(b) - ); - const orderedSpendSigners = spendSigners.sort((a, b) => - Buffer.from(a).compare(Buffer.from(b)) - ); + if (providerSigners.length === 0) throw new Error("No Provider signatures added"); - if (ortderedProviderSigners.length === 0) { - throw new Error("No Provider signatures added"); - } - - // MAPs must always be ordered by key so here it is providers -> P256 and each one ordered by pk - const signatures = xdr.ScVal.scvVec([ - xdr.ScVal.scvMap([ - ...orderedSpendSigners.map((utxo) => { - const { sig, exp } = this.innerSignatures.get(utxo)!; - - return new xdr.ScMapEntry({ - key: xdr.ScVal.scvVec([ - xdr.ScVal.scvSymbol("P256"), - xdr.ScVal.scvBytes(Buffer.from(utxo)), - ]), - val: xdr.ScVal.scvVec([ - xdr.ScVal.scvVec([ - xdr.ScVal.scvSymbol("P256"), - xdr.ScVal.scvBytes(sig), - ]), - - xdr.ScVal.scvU32(exp), - ]), - }); - }), - ...ortderedProviderSigners.map((pk) => { - const { sig, exp } = this.providerInnerSignatures.get(pk)!; - - return new xdr.ScMapEntry({ - key: xdr.ScVal.scvVec([ - xdr.ScVal.scvSymbol("Provider"), - xdr.ScVal.scvBytes(StrKey.decodeEd25519PublicKey(pk)), - ]), - val: xdr.ScVal.scvVec([ - xdr.ScVal.scvVec([ - xdr.ScVal.scvSymbol("Ed25519"), - xdr.ScVal.scvBytes(sig), - ]), - - xdr.ScVal.scvU32(exp), - ]), - }); - }), - ]), - ]); + const spendSigs = Array.from(this.innerSignatures.entries()).map(([utxo, { sig, exp }]) => ({ utxo, sig, exp })); + const providerSigs = providerSigners.map((pk) => { + const { sig, exp } = this.providerInnerSignatures.get(pk)!; + return { pubKey: pk, sig, exp }; + }); - return signatures.toXDR("base64"); + return buildSignaturesXDR(spendSigs, providerSigs); } async signWithProvider( diff --git a/src/transaction-builder/index.unit.test.ts b/src/transaction-builder/index.unit.test.ts index 608b1a0..544eb80 100644 --- a/src/transaction-builder/index.unit.test.ts +++ b/src/transaction-builder/index.unit.test.ts @@ -2,25 +2,27 @@ import { assertEquals, assertThrows, -} from "https://deno.land/std@0.220.1/assert/mod.ts"; -import { MoonlightTransactionBuilder, createOpToXDR, depositOpToXDR, withdrawOpToXDR, spendOpToXDR } from "./index.ts"; +} from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { MoonlightTransactionBuilder } from "./index.ts"; +import { createOpToXDR, depositOpToXDR, withdrawOpToXDR, spendOpToXDR } from "./xdr/index.ts"; import { Asset, Keypair, StrKey, xdr } from "@stellar/stellar-sdk"; +import { Buffer } from "buffer"; import { Condition } from "../conditions/types.ts"; import { StellarSmartContractId } from "../utils/types/stellar.types.ts"; // Mock data for testing -const mockChannelId: StellarSmartContractId = StrKey.encodeContract(new Uint8Array(32)) as StellarSmartContractId; -const mockAuthId: StellarSmartContractId = StrKey.encodeContract(new Uint8Array(32).fill(1)) as StellarSmartContractId; +const mockChannelId: StellarSmartContractId = StrKey.encodeContract(Buffer.alloc(32)) as StellarSmartContractId; +const mockAuthId: StellarSmartContractId = StrKey.encodeContract(Buffer.alloc(32, 1)) as StellarSmartContractId; const mockNetwork = "testnet"; const mockAsset = Asset.native(); // Mock UTXO data -const mockUTXO1 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); -const mockUTXO2 = new Uint8Array([9, 10, 11, 12, 13, 14, 15, 16]); +const mockUTXO1 = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); +const mockUTXO2 = Buffer.from([9, 10, 11, 12, 13, 14, 15, 16]); // Mock Ed25519 public keys -const mockEd25519Key1 = Keypair.random().publicKey(); -const mockEd25519Key2 = Keypair.random().publicKey(); +const mockEd25519Key1 = Keypair.random().publicKey() as `G${string}`; +const mockEd25519Key2 = Keypair.random().publicKey() as `G${string}`; // Mock conditions const mockCreateCondition: Condition = { @@ -272,7 +274,7 @@ Deno.test("MoonlightTransactionBuilder - Basic Operations (Add Methods)", async Deno.test("MoonlightTransactionBuilder - Internal Signatures", async (t) => { await t.step("addInnerSignature should add signature for existing spend operation", () => { const builder = createTestBuilder(); - const mockSignature = new Uint8Array(64).fill(0x42); + const mockSignature = Buffer.alloc(64, 0x42); const expirationLedger = 1000; // First add a spend operation @@ -290,7 +292,7 @@ Deno.test("MoonlightTransactionBuilder - Internal Signatures", async (t) => { await t.step("addInnerSignature should throw error when UTXO not found in spend operations", () => { const builder = createTestBuilder(); - const mockSignature = new Uint8Array(64).fill(0x42); + const mockSignature = Buffer.alloc(64, 0x42); const expirationLedger = 1000; // Don't add any spend operations @@ -305,7 +307,7 @@ Deno.test("MoonlightTransactionBuilder - Internal Signatures", async (t) => { await t.step("addProviderInnerSignature should add provider signature", () => { const builder = createTestBuilder(); - const mockSignature = new Uint8Array(64).fill(0x43); + const mockSignature = Buffer.alloc(64, 0x43); const expirationLedger = 1000; const nonce = "123456789"; @@ -371,7 +373,7 @@ Deno.test("MoonlightTransactionBuilder - Internal Signatures", async (t) => { await t.step("should allow chaining signature operations", () => { const builder = createTestBuilder(); - const mockSignature = new Uint8Array(64).fill(0x44); + const mockSignature = Buffer.alloc(64, 0x44); const mockAuthEntry = {} as xdr.SorobanAuthorizationEntry; // Add operations first @@ -392,8 +394,8 @@ Deno.test("MoonlightTransactionBuilder - Internal Signatures", async (t) => { await t.step("should handle multiple provider signatures", () => { const builder = createTestBuilder(); - const mockSignature1 = new Uint8Array(64).fill(0x45); - const mockSignature2 = new Uint8Array(64).fill(0x46); + const mockSignature1 = Buffer.alloc(64, 0x45); + const mockSignature2 = Buffer.alloc(64, 0x46); const result = builder .addProviderInnerSignature(mockEd25519Key1, mockSignature1, 1000, "nonce1") @@ -408,8 +410,8 @@ Deno.test("MoonlightTransactionBuilder - Internal Signatures", async (t) => { await t.step("should handle multiple inner signatures for different UTXOs", () => { const builder = createTestBuilder(); - const mockSignature1 = new Uint8Array(64).fill(0x47); - const mockSignature2 = new Uint8Array(64).fill(0x48); + const mockSignature1 = Buffer.alloc(64, 0x47); + const mockSignature2 = Buffer.alloc(64, 0x48); // Add spend operations for different UTXOs builder.addSpend(mockUTXO1, [mockCreateCondition]); @@ -576,7 +578,7 @@ Deno.test("MoonlightTransactionBuilder - Hash and Signature XDR", async (t) => { await t.step("signaturesXDR should return correct XDR format", () => { const builder = createTestBuilder(); - const mockSignature = new Uint8Array(64).fill(0x42); + const mockSignature = Buffer.alloc(64, 0x42); // Add provider signature builder.addProviderInnerSignature(mockEd25519Key1, mockSignature, 1000, "nonce123"); @@ -589,8 +591,8 @@ Deno.test("MoonlightTransactionBuilder - Hash and Signature XDR", async (t) => { await t.step("signaturesXDR should order signatures correctly", () => { const builder = createTestBuilder(); - const mockSignature1 = new Uint8Array(64).fill(0x42); - const mockSignature2 = new Uint8Array(64).fill(0x43); + const mockSignature1 = Buffer.alloc(64, 0x42); + const mockSignature2 = Buffer.alloc(64, 0x43); // Add provider signatures in reverse order builder @@ -605,7 +607,7 @@ Deno.test("MoonlightTransactionBuilder - Hash and Signature XDR", async (t) => { await t.step("signaturesXDR should handle both provider and spend signatures", () => { const builder = createTestBuilder(); - const mockSignature = new Uint8Array(64).fill(0x44); + const mockSignature = Buffer.alloc(64, 0x44); // Add spend operation and signatures builder @@ -652,7 +654,8 @@ Deno.test("MoonlightTransactionBuilder - High-Level Signing Methods", async (t) const builder = createTestBuilder(); const mockUtxo = { publicKey: mockUTXO1, - signPayload: async (payload: Uint8Array) => new Uint8Array(64).fill(0x42) + privateKey: Buffer.alloc(32, 0x01), + signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42) }; const expirationLedger = 1000; @@ -661,7 +664,7 @@ Deno.test("MoonlightTransactionBuilder - High-Level Signing Methods", async (t) await builder.signWithSpendUtxo(mockUtxo, expirationLedger); } catch (error) { errorThrown = true; - assertEquals(error.message, "No spend operation for this UTXO"); + assertEquals((error as Error).message, "No spend operation for this UTXO"); } assertEquals(errorThrown, true); }); @@ -670,7 +673,8 @@ Deno.test("MoonlightTransactionBuilder - High-Level Signing Methods", async (t) const builder = createTestBuilder(); const mockUtxo = { publicKey: mockUTXO1, - signPayload: async (payload: Uint8Array) => new Uint8Array(64).fill(0x42) + privateKey: Buffer.alloc(32, 0x01), + signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42) }; const expirationLedger = 1000; @@ -689,7 +693,7 @@ Deno.test("MoonlightTransactionBuilder - High-Level Signing Methods", async (t) const expirationLedger = 1000; // Add deposit operation first - builder.addDeposit(keypair.publicKey(), 500n, [mockDepositCondition]); + builder.addDeposit(keypair.publicKey() as `G${string}`, 500n, [mockDepositCondition]); await builder.signExtWithEd25519(keypair, expirationLedger); @@ -704,7 +708,7 @@ Deno.test("MoonlightTransactionBuilder - High-Level Signing Methods", async (t) const customNonce = "555444333"; // Add deposit operation first - builder.addDeposit(keypair.publicKey(), 500n, [mockDepositCondition]); + builder.addDeposit(keypair.publicKey() as `G${string}`, 500n, [mockDepositCondition]); await builder.signExtWithEd25519(keypair, expirationLedger, customNonce); @@ -718,14 +722,15 @@ Deno.test("MoonlightTransactionBuilder - High-Level Signing Methods", async (t) const userKeypair = Keypair.random(); const mockUtxo = { publicKey: mockUTXO1, - signPayload: async (payload: Uint8Array) => new Uint8Array(64).fill(0x42) + privateKey: Buffer.alloc(32, 0x01), + signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42) }; const expirationLedger = 1000; // Add operations builder .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(userKeypair.publicKey(), 500n, [mockDepositCondition]); + .addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [mockDepositCondition]); // Sign with all methods (now that buildAuthPayloadHash is implemented) await builder.signWithProvider(providerKeypair, expirationLedger); @@ -747,7 +752,7 @@ Deno.test("MoonlightTransactionBuilder - Final Methods", async (t) => { const expirationLedger = 1000; // Add operations and sign - builder.addDeposit(userKeypair.publicKey(), 500n, [mockDepositCondition]); + builder.addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [mockDepositCondition]); await builder.signWithProvider(providerKeypair, expirationLedger); await builder.signExtWithEd25519(userKeypair, expirationLedger); @@ -765,7 +770,7 @@ Deno.test("MoonlightTransactionBuilder - Final Methods", async (t) => { const expirationLedger = 1000; // Add operations and sign - builder.addDeposit(userKeypair.publicKey(), 500n, [mockDepositCondition]); + builder.addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [mockDepositCondition]); await builder.signWithProvider(providerKeypair, expirationLedger); await builder.signExtWithEd25519(userKeypair, expirationLedger); @@ -830,7 +835,8 @@ Deno.test("MoonlightTransactionBuilder - Final Methods", async (t) => { const userKeypair = Keypair.random(); const mockUtxo = { publicKey: mockUTXO1, - signPayload: async (payload: Uint8Array) => new Uint8Array(64).fill(0x42) + privateKey: Buffer.alloc(32, 0x01), + signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42) }; const expirationLedger = 1000; @@ -838,8 +844,8 @@ Deno.test("MoonlightTransactionBuilder - Final Methods", async (t) => { builder .addCreate(mockUTXO1, 1000n) .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(userKeypair.publicKey(), 500n, [mockDepositCondition]) - .addWithdraw(userKeypair.publicKey(), 200n, [mockWithdrawCondition]); + .addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [mockDepositCondition]) + .addWithdraw(userKeypair.publicKey() as `G${string}`, 200n, [mockWithdrawCondition]); // Sign with all methods await builder.signWithProvider(providerKeypair, expirationLedger); @@ -1020,11 +1026,13 @@ Deno.test("MoonlightTransactionBuilder - Integration and Edge Cases", async (t) const userKeypair2 = Keypair.random(); const mockUtxo1 = { publicKey: mockUTXO1, - signPayload: async (payload: Uint8Array) => new Uint8Array(64).fill(0x42) + privateKey: Buffer.alloc(32, 0x01), + signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42) }; const mockUtxo2 = { publicKey: mockUTXO2, - signPayload: async (payload: Uint8Array) => new Uint8Array(64).fill(0x43) + privateKey: Buffer.alloc(32, 0x02), + signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x43) }; const expirationLedger = 1000; @@ -1034,10 +1042,10 @@ Deno.test("MoonlightTransactionBuilder - Integration and Edge Cases", async (t) .addCreate(mockUTXO2, 2000n) .addSpend(mockUTXO1, [mockCreateCondition]) .addSpend(mockUTXO2, [mockDepositCondition, mockWithdrawCondition]) - .addDeposit(userKeypair1.publicKey(), 500n, [mockDepositCondition]) - .addDeposit(userKeypair2.publicKey(), 300n, [mockWithdrawCondition]) - .addWithdraw(userKeypair1.publicKey(), 200n, [mockWithdrawCondition]) - .addWithdraw(userKeypair2.publicKey(), 100n, [mockCreateCondition]); + .addDeposit(userKeypair1.publicKey() as `G${string}`, 500n, [mockDepositCondition]) + .addDeposit(userKeypair2.publicKey() as `G${string}`, 300n, [mockWithdrawCondition]) + .addWithdraw(userKeypair1.publicKey() as `G${string}`, 200n, [mockWithdrawCondition]) + .addWithdraw(userKeypair2.publicKey() as `G${string}`, 100n, [mockCreateCondition]); // Sign with all methods await builder.signWithProvider(providerKeypair, expirationLedger); @@ -1081,12 +1089,12 @@ Deno.test("MoonlightTransactionBuilder - Integration and Edge Cases", async (t) builder .addCreate(mockUTXO1, 1000n) .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(userKeypair1.publicKey(), 500n, [mockDepositCondition]) - .addDeposit(userKeypair2.publicKey(), 300n, [mockWithdrawCondition]) - .addDeposit(userKeypair3.publicKey(), 200n, [mockWithdrawCondition]) - .addWithdraw(userKeypair1.publicKey(), 200n, [mockWithdrawCondition]) - .addWithdraw(userKeypair2.publicKey(), 100n, [mockCreateCondition]) - .addWithdraw(userKeypair3.publicKey(), 150n, [mockDepositCondition]); + .addDeposit(userKeypair1.publicKey() as `G${string}`, 500n, [mockDepositCondition]) + .addDeposit(userKeypair2.publicKey() as `G${string}`, 300n, [mockWithdrawCondition]) + .addDeposit(userKeypair3.publicKey() as `G${string}`, 200n, [mockWithdrawCondition]) + .addWithdraw(userKeypair1.publicKey() as `G${string}`, 200n, [mockWithdrawCondition]) + .addWithdraw(userKeypair2.publicKey() as `G${string}`, 100n, [mockCreateCondition]) + .addWithdraw(userKeypair3.publicKey() as `G${string}`, 150n, [mockDepositCondition]); // Add multiple provider signatures await builder.signWithProvider(providerKeypair1, expirationLedger); @@ -1113,7 +1121,8 @@ Deno.test("MoonlightTransactionBuilder - Integration and Edge Cases", async (t) const userKeypair = Keypair.random(); const mockUtxo = { publicKey: mockUTXO1, - signPayload: async (payload: Uint8Array) => new Uint8Array(64).fill(0x42) + privateKey: Buffer.alloc(32, 0x01), + signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42) }; const expirationLedger = 1000; @@ -1121,7 +1130,7 @@ Deno.test("MoonlightTransactionBuilder - Integration and Edge Cases", async (t) builder .addCreate(mockUTXO1, 1000n) .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(userKeypair.publicKey(), 500n, [mockDepositCondition]); + .addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [mockDepositCondition]); await builder.signWithProvider(providerKeypair, expirationLedger); await builder.signWithSpendUtxo(mockUtxo, expirationLedger); @@ -1158,8 +1167,8 @@ Deno.test("MoonlightTransactionBuilder - Integration and Edge Cases", async (t) builder .addCreate(utxo, BigInt(1000 + i)) .addSpend(utxo, [mockCreateCondition]) - .addDeposit(keypair.publicKey(), BigInt(500 + i), [mockDepositCondition]) - .addWithdraw(keypair.publicKey(), BigInt(300 + i), [mockWithdrawCondition]); + .addDeposit(keypair.publicKey() as `G${string}`, BigInt(500 + i), [mockDepositCondition]) + .addWithdraw(keypair.publicKey() as `G${string}`, BigInt(300 + i), [mockWithdrawCondition]); } const operations = builder.getOperation(); @@ -1233,7 +1242,7 @@ Deno.test("MoonlightTransactionBuilder - Integration and Edge Cases", async (t) ); // Test with empty public key - should work but be a valid key - const emptyKey = "G" + "A".repeat(55); // Valid format but empty content + const emptyKey = ("G" + "A".repeat(55)) as `G${string}`; // Valid format but empty content builder.addDeposit(emptyKey, 500n, []); // Try to add the same public key again - should throw error @@ -1250,7 +1259,8 @@ Deno.test("MoonlightTransactionBuilder - Integration and Edge Cases", async (t) const userKeypair = Keypair.random(); const mockUtxo = { publicKey: mockUTXO1, - signPayload: async (payload: Uint8Array) => new Uint8Array(64).fill(0x42) + privateKey: Buffer.alloc(32, 0x01), + signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42) }; const expirationLedger = 1000; @@ -1258,7 +1268,7 @@ Deno.test("MoonlightTransactionBuilder - Integration and Edge Cases", async (t) builder .addCreate(mockUTXO1, 1000n) .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(userKeypair.publicKey(), 500n, [mockDepositCondition]); + .addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [mockDepositCondition]); // Sign concurrently (simulate concurrent access) const signingPromises = [ @@ -1289,8 +1299,8 @@ Deno.test("MoonlightTransactionBuilder - Integration and Edge Cases", async (t) builder .addCreate(largeUtxo, largeAmount) .addSpend(largeUtxo, [mockCreateCondition, mockDepositCondition, mockWithdrawCondition]) - .addDeposit(keypair.publicKey(), largeAmount, [mockDepositCondition]) - .addWithdraw(keypair.publicKey(), largeAmount, [mockWithdrawCondition]); + .addDeposit(keypair.publicKey() as `G${string}`, largeAmount, [mockDepositCondition]) + .addWithdraw(keypair.publicKey() as `G${string}`, largeAmount, [mockWithdrawCondition]); const operations = builder.getOperation(); const xdr = builder.buildXDR(); diff --git a/src/transaction-builder/signatures/index.ts b/src/transaction-builder/signatures/index.ts new file mode 100644 index 0000000..9e7caca --- /dev/null +++ b/src/transaction-builder/signatures/index.ts @@ -0,0 +1 @@ +export * from "./signatures-xdr.ts"; diff --git a/src/transaction-builder/signatures/signatures-xdr.ts b/src/transaction-builder/signatures/signatures-xdr.ts new file mode 100644 index 0000000..409742f --- /dev/null +++ b/src/transaction-builder/signatures/signatures-xdr.ts @@ -0,0 +1,64 @@ +import { xdr, StrKey } from "@stellar/stellar-sdk"; +import { Buffer } from "buffer"; + +export type SpendInnerSignature = { utxo: Uint8Array; sig: Buffer; exp: number }; +export type ProviderInnerSignature = { + pubKey: string; + sig: Buffer; + exp: number; +}; + +export const buildSignaturesXDR = ( + spendSignatures: SpendInnerSignature[], + providerSignatures: ProviderInnerSignature[], +): string => { + const orderedSpendSigners = [...spendSignatures].sort((a, b) => + Buffer.from(a.utxo).compare(Buffer.from(b.utxo)) + ); + const orderedProviderSigners = [...providerSignatures].sort((a, b) => + a.pubKey.localeCompare(b.pubKey) + ); + + const entries: xdr.ScMapEntry[] = []; + + for (const { utxo, sig, exp } of orderedSpendSigners) { + entries.push( + new xdr.ScMapEntry({ + key: xdr.ScVal.scvVec([ + xdr.ScVal.scvSymbol("P256"), + xdr.ScVal.scvBytes(Buffer.from(utxo)), + ]), + val: xdr.ScVal.scvVec([ + xdr.ScVal.scvVec([ + xdr.ScVal.scvSymbol("P256"), + xdr.ScVal.scvBytes(sig), + ]), + xdr.ScVal.scvU32(exp), + ]), + }), + ); + } + + for (const { pubKey, sig, exp } of orderedProviderSigners) { + entries.push( + new xdr.ScMapEntry({ + key: xdr.ScVal.scvVec([ + xdr.ScVal.scvSymbol("Provider"), + xdr.ScVal.scvBytes(StrKey.decodeEd25519PublicKey(pubKey)), + ]), + val: xdr.ScVal.scvVec([ + xdr.ScVal.scvVec([ + xdr.ScVal.scvSymbol("Ed25519"), + xdr.ScVal.scvBytes(sig), + ]), + xdr.ScVal.scvU32(exp), + ]), + }), + ); + } + + const signatures = xdr.ScVal.scvVec([xdr.ScVal.scvMap(entries)]); + return signatures.toXDR("base64"); +}; + + diff --git a/src/transaction-builder/utils/index.ts b/src/transaction-builder/utils/index.ts new file mode 100644 index 0000000..8a58287 --- /dev/null +++ b/src/transaction-builder/utils/index.ts @@ -0,0 +1 @@ +export * from "./ordering.ts"; diff --git a/src/transaction-builder/utils/ordering.ts b/src/transaction-builder/utils/ordering.ts new file mode 100644 index 0000000..3d7c58f --- /dev/null +++ b/src/transaction-builder/utils/ordering.ts @@ -0,0 +1,10 @@ +import { Buffer } from "buffer"; +import { SpendOperation } from "../types.ts"; + +export const orderSpendByUtxo = (spend: SpendOperation[]): SpendOperation[] => { + return [...spend].sort((a, b) => + Buffer.from(a.utxo).compare(Buffer.from(b.utxo)) + ); +}; + + diff --git a/src/transaction-builder/validators/index.ts b/src/transaction-builder/validators/index.ts new file mode 100644 index 0000000..d285006 --- /dev/null +++ b/src/transaction-builder/validators/index.ts @@ -0,0 +1 @@ +export * from "./operations.ts"; diff --git a/src/transaction-builder/validators/operations.ts b/src/transaction-builder/validators/operations.ts new file mode 100644 index 0000000..9165017 --- /dev/null +++ b/src/transaction-builder/validators/operations.ts @@ -0,0 +1,42 @@ +import { Buffer } from "buffer"; +import { Condition } from "../../conditions/types.ts"; +import { UTXOPublicKey, Ed25519PublicKey } from "../types.ts"; + +export const assertPositiveAmount = (amount: bigint, context: string) => { + if (amount <= 0n) throw new Error(`${context} amount must be positive`); +}; + +export const assertNoDuplicateCreate = ( + existing: { utxo: UTXOPublicKey }[], + utxo: UTXOPublicKey, +) => { + if (existing.find((c) => Buffer.from(c.utxo).equals(Buffer.from(utxo)))) + throw new Error("Create operation for this UTXO already exists"); +}; + +export const assertNoDuplicateSpend = ( + existing: { utxo: UTXOPublicKey }[], + utxo: UTXOPublicKey, +) => { + if (existing.find((s) => Buffer.from(s.utxo).equals(Buffer.from(utxo)))) + throw new Error("Spend operation for this UTXO already exists"); +}; + +export const assertNoDuplicatePubKey = ( + existing: { pubKey: Ed25519PublicKey }[], + pubKey: Ed25519PublicKey, + context: string, +) => { + if (existing.find((d) => d.pubKey === pubKey)) + throw new Error(`${context} operation for this public key already exists`); +}; + +export const assertSpendExists = ( + existing: { utxo: UTXOPublicKey; conditions: Condition[] }[], + utxo: UTXOPublicKey, +) => { + if (!existing.find((s) => Buffer.from(s.utxo).equals(Buffer.from(utxo)))) + throw new Error("No spend operation for this UTXO"); +}; + + diff --git a/src/transaction-builder/xdr/index.ts b/src/transaction-builder/xdr/index.ts new file mode 100644 index 0000000..51a5bd0 --- /dev/null +++ b/src/transaction-builder/xdr/index.ts @@ -0,0 +1 @@ +export * from "./ops-to-xdr.ts"; diff --git a/src/transaction-builder/xdr/ops-to-xdr.ts b/src/transaction-builder/xdr/ops-to-xdr.ts new file mode 100644 index 0000000..6b43633 --- /dev/null +++ b/src/transaction-builder/xdr/ops-to-xdr.ts @@ -0,0 +1,47 @@ +import { xdr, nativeToScVal } from "@stellar/stellar-sdk"; +import { Buffer } from "buffer"; +import { + CreateOperation, + DepositOperation, + WithdrawOperation, + SpendOperation, +} from "../types.ts"; +import { conditionToXDR } from "../../conditions/index.ts"; + +export const createOpToXDR = (op: CreateOperation): xdr.ScVal => { + return xdr.ScVal.scvVec([ + xdr.ScVal.scvBytes(Buffer.from(op.utxo as Uint8Array)), + nativeToScVal(op.amount, { type: "i128" }), + ]); +}; + +export const depositOpToXDR = (op: DepositOperation): xdr.ScVal => { + return xdr.ScVal.scvVec([ + nativeToScVal(op.pubKey, { type: "address" }), + nativeToScVal(op.amount, { type: "i128" }), + op.conditions.length === 0 + ? xdr.ScVal.scvVec(null) + : xdr.ScVal.scvVec(op.conditions.map((c) => conditionToXDR(c))), + ]); +}; + +export const withdrawOpToXDR = (op: WithdrawOperation): xdr.ScVal => { + return xdr.ScVal.scvVec([ + nativeToScVal(op.pubKey, { type: "address" }), + nativeToScVal(op.amount, { type: "i128" }), + op.conditions.length === 0 + ? xdr.ScVal.scvVec(null) + : xdr.ScVal.scvVec(op.conditions.map((c) => conditionToXDR(c))), + ]); +}; + +export const spendOpToXDR = (op: SpendOperation): xdr.ScVal => { + return xdr.ScVal.scvVec([ + xdr.ScVal.scvBytes(Buffer.from(op.utxo as Uint8Array)), + op.conditions.length === 0 + ? xdr.ScVal.scvVec(null) + : xdr.ScVal.scvVec(op.conditions.map((c) => conditionToXDR(c))), + ]); +}; + + diff --git a/src/utils/auth/build-auth-payload.ts b/src/utils/auth/build-auth-payload.ts index 852f01d..a48e5dc 100644 --- a/src/utils/auth/build-auth-payload.ts +++ b/src/utils/auth/build-auth-payload.ts @@ -18,8 +18,6 @@ export const buildAuthPayloadHash = ({ const encoder = new TextEncoder(); const encodedContractId = encoder.encode(contractId); - // const addr = Address.fromString(contractId); - // const addrXdr = addr.toScAddress().toXDR(); const parts: Uint8Array[] = [encodedContractId]; const createConditions: CreateCondition[] = []; @@ -37,48 +35,33 @@ export const buildAuthPayloadHash = ({ } // CREATE - // parts.push(encoder.encode("CREATE")); for (const createCond of createConditions) { parts.push(new Uint8Array(createCond.utxo)); const amountBytes = bigintToLE(createCond.amount, 16); parts.push(amountBytes); } // DEPOSIT - // parts.push(encoder.encode("DEPOSIT")); for (const depositCond of depositConditions) { - // const addrXdr = Address.fromString(depositCond.publicKey) - // .toScAddress() - // .toXDR(); parts.push(encoder.encode(depositCond.publicKey)); parts.push(bigintToLE(depositCond.amount, 16)); } // WITHDRAW - // parts.push(encoder.encode("WITHDRAW")); for (const withdrawCond of withdrawConditions) { - // const addrXdr = Address.fromString(withdrawCond.publicKey) - // .toScAddress() - // .toXDR(); - // parts.push(new Uint8Array(addrXdr)); parts.push(encoder.encode(withdrawCond.publicKey)); parts.push(bigintToLE(withdrawCond.amount, 16)); } - // parts.push(encoder.encode("INTEGRATE")); - // MOCK - const encodedLiveUntil = bigintToLE(BigInt(liveUntilLedger), 4); parts.push(encodedLiveUntil); // Concatenate all parts into one Uint8Array const payloadBuffer = Buffer.concat(parts); - // const payload = new Uint8Array(payloadBuffer); const payload = Buffer.concat(parts); - // return sha256Buffer(payload); return payload; }; -//convert bigint to little endian +// Convert bigint to little endian export function bigintToLE(amount: bigint, byteLength: number): Uint8Array { const result = new Uint8Array(byteLength); let temp = amount; diff --git a/src/utils/auth/bundle-auth-entry.ts b/src/utils/auth/bundle-auth-entry.ts index 0224f42..f25933d 100644 --- a/src/utils/auth/bundle-auth-entry.ts +++ b/src/utils/auth/bundle-auth-entry.ts @@ -1,10 +1,8 @@ import { xdr } from "@stellar/stellar-sdk"; -import { InvocationParams, paramsToAuthEntry } from "./auth-entries.ts"; - +import { type InvocationParams, paramsToAuthEntry } from "./auth-entries.ts"; export const generateBundleAuthEntry = ({ channelId, - authId, args, nonce, @@ -12,7 +10,6 @@ export const generateBundleAuthEntry = ({ signaturesXdr, }: { channelId: string; - authId: string; args: xdr.ScVal[]; nonce: string; diff --git a/src/utils/common/index.ts b/src/utils/common/index.ts index dfc1f4e..b080f71 100644 --- a/src/utils/common/index.ts +++ b/src/utils/common/index.ts @@ -19,21 +19,3 @@ export function generateNonce(): string { return randomBigInt.toString(); } - -export function highlightText( - text: string, - color: "red" | "green" | "yellow" | "blue" | "cyan" | "magenta" = "cyan" -): string { - const colors: Record = { - red: "\x1b[31m", - green: "\x1b[32m", - yellow: "\x1b[33m", - blue: "\x1b[34m", - magenta: "\x1b[35m", - cyan: "\x1b[36m", - }; - - const reset = "\x1b[0m"; - return `${colors[color]}${text}${reset}`; -} - \ No newline at end of file From 7fa2e44827bf67b627bf91fb1076bf10c93f8b48 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Fri, 17 Oct 2025 10:45:47 -0300 Subject: [PATCH 38/90] feat: Add Condition class for UTXO operations with factory methods and type safety - Refactored the Condition class in `src/conditions/index.ts` to implement a private constructor, enforcing the use of static factory methods for creating conditions (create, deposit, withdraw). - Added detailed JSDoc comments for better documentation and usage examples. - Introduced a new enum `UTXOOperation` to represent operation types (CREATE, DEPOSIT, WITHDRAW) for improved clarity. - Updated the `BaseCondition` type and its derived types (CreateCondition, DepositCondition, WithdrawCondition) in `src/conditions/types.ts` to align with the new structure and ensure type safety. - Removed old type definitions and replaced them with a more structured approach to handling UTXO operations. --- src/conditions/index.ts | 423 +++++++++++++++++++++++++++++++++++----- src/conditions/types.ts | 75 +++++-- 2 files changed, 428 insertions(+), 70 deletions(-) diff --git a/src/conditions/index.ts b/src/conditions/index.ts index 7131b35..16cdf17 100644 --- a/src/conditions/index.ts +++ b/src/conditions/index.ts @@ -1,52 +1,373 @@ -import { Condition } from "./types.ts"; +import { StrKey, type Ed25519PublicKey } from "@colibri/core"; import { nativeToScVal, xdr } from "@stellar/stellar-sdk"; -import { Buffer } from "buffer"; -import { CreateCondition, DepositCondition, WithdrawCondition } from "./types.ts"; - -const actionToSymbolStr = (action: Condition["action"]): string => { - if (action === "CREATE") return "Create"; - if (action === "DEPOSIT") return "Deposit"; - if (action === "WITHDRAW") return "Withdraw"; - throw new Error("Invalid action"); -}; - -export const conditionToXDR = (cond: Condition): xdr.ScVal => { - const actionXDR = xdr.ScVal.scvSymbol(actionToSymbolStr(cond.action)); - const addressXDR = - cond.action === "CREATE" - ? xdr.ScVal.scvBytes(Buffer.from(cond.utxo)) - : nativeToScVal(cond.publicKey, { type: "address" }); - const amountXDR = nativeToScVal(cond.amount, { type: "i128" }); - - const cXDR = xdr.ScVal.scvVec([actionXDR, addressXDR, amountXDR]); - - return cXDR; -}; - -export const createCondition = ( - utxo: Uint8Array, - amount: bigint -): CreateCondition => ({ - action: "CREATE", - utxo, - amount, -}); - -export const depositCondition = ( - publicKey: string, - amount: bigint -): DepositCondition => ({ - action: "DEPOSIT", - publicKey, - amount, -}); - -export const withdrawCondition = ( - publicKey: string, - amount: bigint -): WithdrawCondition => ({ - action: "WITHDRAW", - publicKey, - amount, -}); - \ No newline at end of file +import type { UTXOPublicKey } from "../transaction-builder/types.ts"; +import { Buffer } from "node:buffer"; +import { + UTXOOperation, + type BaseCondition, + type CreateCondition, + type DepositCondition, + type WithdrawCondition, +} from "./types.ts"; + +/** + * Represents a condition for UTXO operations in the Moonlight privacy protocol. + * Conditions define the rules for creating, depositing, and withdrawing funds + * in a privacy-preserving manner on the Stellar blockchain. + * + * This class uses a factory pattern with static methods to create specific + * condition types while maintaining type safety through TypeScript discriminated unions. + * + * @example + * ```typescript + * // Create a new UTXO + * const createCondition = Condition.create(utxoPublicKey, 1000n); + * + * // Deposit funds to a public key + * const depositCondition = Condition.deposit(recipientPublicKey, 500n); + * + * // Withdraw funds to a public key + * const withdrawCondition = Condition.withdraw(recipientPublicKey, 300n); + * ``` + */ +export class Condition implements BaseCondition { + private _op: UTXOOperation; + private _amount: bigint; + private _publicKey?: Ed25519PublicKey; + private _utxo?: UTXOPublicKey; + + /** + * Private constructor to enforce factory pattern usage. + * Use static methods `create()`, `deposit()`, or `withdraw()` instead. + * + * @param params - Configuration object for the condition + * @param params.op - The UTXO operation type + * @param params.amount - The amount in stroops (must be greater than zero) + * @param params.publicKey - Optional Ed25519 public key for deposit/withdraw operations + * @param params.utxo - Optional UTXO public key for create operations + * @throws {Error} If amount is zero or negative + */ + private constructor({ + op, + amount, + publicKey, + utxo, + }: { + op: UTXOOperation; + amount: bigint; + publicKey?: Ed25519PublicKey; + utxo?: UTXOPublicKey; + }) { + if (amount <= 0n) { + throw new Error("Amount must be greater than zero."); + } + + this._op = op; + this._amount = amount; + this._publicKey = publicKey; + this._utxo = utxo; + } + + /** + * Creates a CREATE condition for generating a new UTXO. + * This operation creates a new unspent transaction output in the privacy protocol. + * + * @param utxo - The UTXO public key that will own the created output + * @param amount - The amount to assign to the UTXO in stroops (must be > 0) + * @returns A CreateCondition instance + * @throws {Error} If amount is zero or negative + * + * @example + * ```typescript + * const utxoKey = new Uint8Array(32); // Your UTXO public key + * const condition = Condition.create(utxoKey, 1000n); + * console.log(condition.getOperation()); // "Create" + * console.log(condition.getAmount()); // 1000n + * ``` + */ + static create(utxo: UTXOPublicKey, amount: bigint): CreateCondition { + return new Condition({ + op: UTXOOperation.CREATE, + utxo, + amount, + }) as CreateCondition; + } + + /** + * Creates a DEPOSIT condition for adding funds to a recipient's account. + * This operation transfers funds from the privacy pool to a public Stellar account. + * + * @param publicKey - The Ed25519 public key of the recipient (Stellar address format) + * @param amount - The amount to deposit in stroops (must be > 0) + * @returns A DepositCondition instance + * @throws {Error} If the public key format is invalid + * @throws {Error} If amount is zero or negative + * + * @example + * ```typescript + * const recipientKey = "GBXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; + * const condition = Condition.deposit(recipientKey, 500n); + * console.log(condition.getOperation()); // "Deposit" + * console.log(condition.getPublicKey()); // "GBXXXXXX..." + * ``` + */ + static deposit( + publicKey: Ed25519PublicKey, + amount: bigint + ): DepositCondition { + if (!StrKey.isValidEd25519PublicKey(publicKey)) { + throw new Error("Invalid Ed25519 public key."); + } + + return new Condition({ + op: UTXOOperation.DEPOSIT, + publicKey, + amount, + }) as DepositCondition; + } + + /** + * Creates a WITHDRAW condition for removing funds to a recipient's account. + * This operation transfers funds from the privacy pool to a public Stellar account, + * similar to deposit but with different semantic meaning in the protocol. + * + * @param publicKey - The Ed25519 public key of the recipient (Stellar address format) + * @param amount - The amount to withdraw in stroops (must be > 0) + * @returns A WithdrawCondition instance + * @throws {Error} If the public key format is invalid + * @throws {Error} If amount is zero or negative + * + * @example + * ```typescript + * const recipientKey = "GBXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; + * const condition = Condition.withdraw(recipientKey, 300n); + * console.log(condition.getOperation()); // "Withdraw" + * console.log(condition.isWithdraw()); // true + * ``` + */ + static withdraw( + publicKey: Ed25519PublicKey, + amount: bigint + ): WithdrawCondition { + if (!StrKey.isValidEd25519PublicKey(publicKey)) { + throw new Error("Invalid Ed25519 public key."); + } + return new Condition({ + op: UTXOOperation.WITHDRAW, + publicKey, + amount, + }) as WithdrawCondition; + } + + //========================================== + // Meta Requirement Methods + //========================================== + + /** + * Internal helper method to safely retrieve required properties. + * Uses method overloading to provide type-safe access to private fields. + * + * @param arg - The name of the property to retrieve + * @returns The value of the requested property + * @throws {Error} If the requested property is not set + * @private + */ + private require(arg: "_op"): UTXOOperation; + private require(arg: "_amount"): bigint; + private require(arg: "_publicKey"): Ed25519PublicKey; + private require(arg: "_utxo"): UTXOPublicKey; + private require( + arg: "_op" | "_amount" | "_publicKey" | "_utxo" + ): UTXOOperation | bigint | Ed25519PublicKey | UTXOPublicKey { + if (this[arg]) return this[arg]; + throw new Error(`Property ${arg} is not set in the Condition instance.`); + } + + //========================================== + // Getter Methods + //========================================== + + /** + * Returns the UTXO operation type of this condition. + * + * @returns The operation type (Create, Deposit, or Withdraw) + * @throws {Error} If the operation is not set (should never happen with factory methods) + * + * @example + * ```typescript + * const condition = Condition.create(utxo, 1000n); + * console.log(condition.getOperation()); // "Create" + * ``` + */ + public getOperation(): UTXOOperation { + return this.require("_op"); + } + + /** + * Returns the amount associated with this condition. + * + * @returns The amount in stroops as a bigint + * @throws {Error} If the amount is not set (should never happen with factory methods) + * + * @example + * ```typescript + * const condition = Condition.deposit(publicKey, 500n); + * console.log(condition.getAmount()); // 500n + * ``` + */ + public getAmount(): bigint { + return this.require("_amount"); + } + + /** + * Returns the Ed25519 public key for deposit or withdraw conditions. + * Only valid for DEPOSIT and WITHDRAW operations. + * + * @returns The Ed25519 public key in Stellar address format + * @throws {Error} If called on a CREATE condition or if public key is not set + * + * @example + * ```typescript + * const condition = Condition.deposit(publicKey, 500n); + * console.log(condition.getPublicKey()); // "GBXXXXXX..." + * ``` + */ + public getPublicKey(): Ed25519PublicKey { + return this.require("_publicKey"); + } + + /** + * Returns the UTXO public key for create conditions. + * Only valid for CREATE operations. + * + * @returns The UTXO public key as a Uint8Array + * @throws {Error} If called on a DEPOSIT or WITHDRAW condition or if UTXO is not set + * + * @example + * ```typescript + * const condition = Condition.create(utxo, 1000n); + * console.log(condition.getUtxo()); // Uint8Array(32) [...] + * ``` + */ + public getUtxo(): UTXOPublicKey { + return this.require("_utxo"); + } + + //========================================== + // Conversion Methods + //========================================== + + /** + * Converts this condition to Stellar's ScVal format for smart contract interaction. + * The ScVal format is used by Soroban smart contracts on the Stellar network. + * + * The resulting ScVal is a vector containing: + * - Operation symbol (Create/Deposit/Withdraw) + * - Address (UTXO bytes for CREATE, Stellar address for DEPOSIT/WITHDRAW) + * - Amount as i128 + * + * @returns The condition as a Stellar ScVal + * + * @example + * ```typescript + * const condition = Condition.deposit(publicKey, 500n); + * const scVal = condition.toScVal(); + * // Can now be used in Soroban contract invocations + * ``` + */ + public toScVal(): xdr.ScVal { + const actionScVal = xdr.ScVal.scvSymbol(this.getOperation()); + const addressScVal = + this.getOperation() === UTXOOperation.CREATE + ? xdr.ScVal.scvBytes(Buffer.from(this.getUtxo())) + : nativeToScVal(this.getPublicKey(), { type: "address" }); + const amountScVal = nativeToScVal(this.getAmount(), { type: "i128" }); + + const conditionScVal = xdr.ScVal.scvVec([ + actionScVal, + addressScVal, + amountScVal, + ]); + + return conditionScVal; + } + + /** + * Converts this condition to XDR (External Data Representation) format. + * XDR is the serialization format used by the Stellar network for all data structures. + * + * @returns The condition as a base64-encoded XDR string + * + * @example + * ```typescript + * const condition = Condition.create(utxo, 1000n); + * const xdr = condition.toXDR(); + * console.log(xdr); // "AAAABgAAAA..." (base64 string) + * // Can be transmitted over network or stored + * ``` + */ + public toXDR(): string { + return this.toScVal().toXDR("base64"); + } + + //========================================== + // Type Guard Methods + //========================================== + + /** + * Type guard to check if this condition is a CREATE operation. + * Narrows the TypeScript type to CreateCondition when true. + * + * @returns True if this is a CREATE condition + * + * @example + * ```typescript + * const condition: Condition = getCondition(); + * if (condition.isCreate()) { + * // TypeScript knows this is CreateCondition + * const utxo = condition.getUtxo(); // Safe to call + * } + * ``` + */ + public isCreate(): this is CreateCondition { + return this.getOperation() === UTXOOperation.CREATE; + } + + /** + * Type guard to check if this condition is a DEPOSIT operation. + * Narrows the TypeScript type to DepositCondition when true. + * + * @returns True if this is a DEPOSIT condition + * + * @example + * ```typescript + * const condition: Condition = getCondition(); + * if (condition.isDeposit()) { + * // TypeScript knows this is DepositCondition + * const key = condition.getPublicKey(); // Safe to call + * } + * ``` + */ + public isDeposit(): this is DepositCondition { + return this.getOperation() === UTXOOperation.DEPOSIT; + } + + /** + * Type guard to check if this condition is a WITHDRAW operation. + * Narrows the TypeScript type to WithdrawCondition when true. + * + * @returns True if this is a WITHDRAW condition + * + * @example + * ```typescript + * const condition: Condition = getCondition(); + * if (condition.isWithdraw()) { + * // TypeScript knows this is WithdrawCondition + * const key = condition.getPublicKey(); // Safe to call + * } + * ``` + */ + public isWithdraw(): this is WithdrawCondition { + return this.getOperation() === UTXOOperation.WITHDRAW; + } +} diff --git a/src/conditions/types.ts b/src/conditions/types.ts index ffd9d1f..eea5b85 100644 --- a/src/conditions/types.ts +++ b/src/conditions/types.ts @@ -1,19 +1,56 @@ -export type CreateCondition = { - action: "CREATE"; - utxo: Uint8Array; - amount: bigint; - }; - - export type DepositCondition = { - action: "DEPOSIT"; - publicKey: string; - amount: bigint; - }; - - export type WithdrawCondition = { - action: "WITHDRAW"; - publicKey: string; - amount: bigint; - }; - - export type Condition = CreateCondition | DepositCondition | WithdrawCondition; \ No newline at end of file +// export type CreateCondition = { +// action: "CREATE"; +// utxo: Uint8Array; +// amount: bigint; +// }; + +import type { Ed25519PublicKey } from "@colibri/core"; +import type { UTXOPublicKey } from "../transaction-builder/types.ts"; +import type { xdr } from "@stellar/stellar-sdk"; + +// export type DepositCondition = { +// action: "DEPOSIT"; +// publicKey: string; +// amount: bigint; +// }; + +// export type WithdrawCondition = { +// action: "WITHDRAW"; +// publicKey: string; +// amount: bigint; +// }; + +// export type Condition = CreateCondition | DepositCondition | WithdrawCondition; + +export enum UTXOOperation { + CREATE = "Create", + DEPOSIT = "Deposit", + WITHDRAW = "Withdraw", +} + +export type BaseCondition = { + getOperation(): UTXOOperation; + getAmount(): bigint; + isCreate(): this is CreateCondition; + isDeposit(): this is DepositCondition; + isWithdraw(): this is WithdrawCondition; + toXDR(): string; + toScVal(): xdr.ScVal; +}; + +export type CreateCondition = BaseCondition & { + getOperation(): UTXOOperation.CREATE; + getUtxo(): UTXOPublicKey; +}; + +export type DepositCondition = BaseCondition & { + getOperation(): UTXOOperation.DEPOSIT; + getPublicKey(): Ed25519PublicKey; +}; + +export type WithdrawCondition = BaseCondition & { + getOperation(): UTXOOperation.WITHDRAW; + getPublicKey(): Ed25519PublicKey; +}; + +export type Condition = CreateCondition | DepositCondition | WithdrawCondition; From 5ba986d06ce300799f54f2e1a037b392c9710024 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Fri, 17 Oct 2025 10:46:02 -0300 Subject: [PATCH 39/90] refactor: Improve type imports and condition handling in transaction builder - Changed imports to use `type` for better clarity and performance in `src/transaction-builder/index.ts`, `src/transaction-builder/types.ts`, and `src/transaction-builder/xdr/ops-to-xdr.ts`. - Updated condition handling in `buildAuthPayloadHash` to use method calls (`isCreate`, `isDeposit`, `isWithdraw`) instead of property checks for improved readability and encapsulation. - Refactored condition mapping to use `toScVal()` method in `src/transaction-builder/xdr/ops-to-xdr.ts` for consistency in converting conditions to XDR format. --- src/transaction-builder/index.ts | 67 ++++++++++++++++------- src/transaction-builder/types.ts | 4 +- src/transaction-builder/xdr/ops-to-xdr.ts | 11 ++-- src/utils/auth/build-auth-payload.ts | 21 ++++--- 4 files changed, 62 insertions(+), 41 deletions(-) diff --git a/src/transaction-builder/index.ts b/src/transaction-builder/index.ts index 8524c30..ee0b94d 100644 --- a/src/transaction-builder/index.ts +++ b/src/transaction-builder/index.ts @@ -1,23 +1,45 @@ import { - Asset, + type Asset, authorizeEntry, - Keypair, + type Keypair, xdr, } from "@stellar/stellar-sdk"; import { Buffer } from "buffer"; -import { CreateOperation, DepositOperation, SpendOperation, WithdrawOperation, UTXOPublicKey, Ed25519PublicKey } from "../transaction-builder/types.ts"; -import { StellarSmartContractId } from "../utils/types/stellar.types.ts"; -import { Condition } from "../conditions/types.ts"; +import type { + CreateOperation, + DepositOperation, + SpendOperation, + WithdrawOperation, + UTXOPublicKey, + Ed25519PublicKey, +} from "../transaction-builder/types.ts"; +import type { StellarSmartContractId } from "../utils/types/stellar.types.ts"; import { generateNonce } from "../utils/common/index.ts"; -import { conditionToXDR } from "../conditions/index.ts"; -import { MoonlightOperation } from "../transaction-builder/types.ts"; + +import type { MoonlightOperation } from "../transaction-builder/types.ts"; import { buildAuthPayloadHash } from "../utils/auth/build-auth-payload.ts"; -import { IUTXOKeypairBase } from "../core/utxo-keypair-base/types.ts"; -import { createOpToXDR, depositOpToXDR, withdrawOpToXDR, spendOpToXDR } from "./xdr/index.ts"; +import type { IUTXOKeypairBase } from "../core/utxo-keypair-base/types.ts"; +import { + createOpToXDR, + depositOpToXDR, + withdrawOpToXDR, + spendOpToXDR, +} from "./xdr/index.ts"; import { buildSignaturesXDR } from "./signatures/index.ts"; -import { buildBundleAuthEntry, buildDepositAuthEntry, buildOperationAuthEntryHash } from "./auth/index.ts"; +import { + buildBundleAuthEntry, + buildDepositAuthEntry, + buildOperationAuthEntryHash, +} from "./auth/index.ts"; import { orderSpendByUtxo } from "./utils/index.ts"; -import { assertPositiveAmount, assertNoDuplicateCreate, assertNoDuplicateSpend, assertNoDuplicatePubKey, assertSpendExists } from "./validators/index.ts"; +import { + assertPositiveAmount, + assertNoDuplicateCreate, + assertNoDuplicateSpend, + assertNoDuplicatePubKey, + assertSpendExists, +} from "./validators/index.ts"; +import type { Condition } from "../conditions/types.ts"; export class MoonlightTransactionBuilder { private create: CreateOperation[] = []; @@ -28,10 +50,8 @@ export class MoonlightTransactionBuilder { private authId: StellarSmartContractId; private asset: Asset; private network: string; - private innerSignatures: Map< - Uint8Array, - { sig: Buffer; exp: number } - > = new Map(); + private innerSignatures: Map = + new Map(); private providerInnerSignatures: Map< Ed25519PublicKey, { sig: Buffer; exp: number; nonce: string } @@ -162,7 +182,9 @@ export class MoonlightTransactionBuilder { assetId: this.asset.contractId(this.network), depositor: address, amount: deposit.amount, - conditions: [xdr.ScVal.scvVec(deposit.conditions.map(conditionToXDR))], + conditions: [ + xdr.ScVal.scvVec(deposit.conditions.map((c) => c.toScVal())), + ], nonce, signatureExpirationLedger, }); @@ -182,7 +204,7 @@ export class MoonlightTransactionBuilder { xdr.ScVal.scvSymbol("P256"), xdr.ScVal.scvBytes(Buffer.from(spend.utxo as Uint8Array)), ]), - val: xdr.ScVal.scvVec(spend.conditions.map(conditionToXDR)), + val: xdr.ScVal.scvVec(spend.conditions.map((c) => c.toScVal())), }) ); } @@ -236,7 +258,7 @@ export class MoonlightTransactionBuilder { nonce, signatureExpirationLedger ).rootInvocation(); - return buildOperationAuthEntryHash({ + return await buildOperationAuthEntryHash({ network: this.network, rootInvocation, nonce, @@ -246,9 +268,12 @@ export class MoonlightTransactionBuilder { signaturesXDR(): string { const providerSigners = Array.from(this.providerInnerSignatures.keys()); - if (providerSigners.length === 0) throw new Error("No Provider signatures added"); + if (providerSigners.length === 0) + throw new Error("No Provider signatures added"); - const spendSigs = Array.from(this.innerSignatures.entries()).map(([utxo, { sig, exp }]) => ({ utxo, sig, exp })); + const spendSigs = Array.from(this.innerSignatures.entries()).map( + ([utxo, { sig, exp }]) => ({ utxo, sig, exp }) + ); const providerSigs = providerSigners.map((pk) => { const { sig, exp } = this.providerInnerSignatures.get(pk)!; return { pubKey: pk, sig, exp }; @@ -354,4 +379,4 @@ export class MoonlightTransactionBuilder { }), ]); } -} \ No newline at end of file +} diff --git a/src/transaction-builder/types.ts b/src/transaction-builder/types.ts index bb241c1..9a93029 100644 --- a/src/transaction-builder/types.ts +++ b/src/transaction-builder/types.ts @@ -1,4 +1,4 @@ -import { Condition } from "../conditions/types.ts"; +import type { Condition } from "../conditions/types.ts"; export type MoonlightOperation = { create: CreateOperation[]; @@ -25,4 +25,4 @@ export type CreateOperation = { utxo: UTXOPublicKey; amount: bigint }; export type UTXOPublicKey = Uint8Array; -export type Ed25519PublicKey = `G${string}`; \ No newline at end of file +export type Ed25519PublicKey = `G${string}`; diff --git a/src/transaction-builder/xdr/ops-to-xdr.ts b/src/transaction-builder/xdr/ops-to-xdr.ts index 6b43633..5268bd8 100644 --- a/src/transaction-builder/xdr/ops-to-xdr.ts +++ b/src/transaction-builder/xdr/ops-to-xdr.ts @@ -1,12 +1,11 @@ import { xdr, nativeToScVal } from "@stellar/stellar-sdk"; import { Buffer } from "buffer"; -import { +import type { CreateOperation, DepositOperation, WithdrawOperation, SpendOperation, } from "../types.ts"; -import { conditionToXDR } from "../../conditions/index.ts"; export const createOpToXDR = (op: CreateOperation): xdr.ScVal => { return xdr.ScVal.scvVec([ @@ -21,7 +20,7 @@ export const depositOpToXDR = (op: DepositOperation): xdr.ScVal => { nativeToScVal(op.amount, { type: "i128" }), op.conditions.length === 0 ? xdr.ScVal.scvVec(null) - : xdr.ScVal.scvVec(op.conditions.map((c) => conditionToXDR(c))), + : xdr.ScVal.scvVec(op.conditions.map((c) => c.toScVal())), ]); }; @@ -31,7 +30,7 @@ export const withdrawOpToXDR = (op: WithdrawOperation): xdr.ScVal => { nativeToScVal(op.amount, { type: "i128" }), op.conditions.length === 0 ? xdr.ScVal.scvVec(null) - : xdr.ScVal.scvVec(op.conditions.map((c) => conditionToXDR(c))), + : xdr.ScVal.scvVec(op.conditions.map((c) => c.toScVal())), ]); }; @@ -40,8 +39,6 @@ export const spendOpToXDR = (op: SpendOperation): xdr.ScVal => { xdr.ScVal.scvBytes(Buffer.from(op.utxo as Uint8Array)), op.conditions.length === 0 ? xdr.ScVal.scvVec(null) - : xdr.ScVal.scvVec(op.conditions.map((c) => conditionToXDR(c))), + : xdr.ScVal.scvVec(op.conditions.map((c) => c.toScVal())), ]); }; - - diff --git a/src/utils/auth/build-auth-payload.ts b/src/utils/auth/build-auth-payload.ts index a48e5dc..f6e59f4 100644 --- a/src/utils/auth/build-auth-payload.ts +++ b/src/utils/auth/build-auth-payload.ts @@ -1,5 +1,5 @@ import { Buffer } from "node:buffer"; -import { +import type { Condition, CreateCondition, DepositCondition, @@ -25,37 +25,36 @@ export const buildAuthPayloadHash = ({ const withdrawConditions: WithdrawCondition[] = []; for (const condition of conditions) { - if (condition.action === "CREATE") { + if (condition.isCreate()) { createConditions.push(condition); - } else if (condition.action === "DEPOSIT") { + } else if (condition.isDeposit()) { depositConditions.push(condition); - } else if (condition.action === "WITHDRAW") { + } else if (condition.isWithdraw()) { withdrawConditions.push(condition); } } // CREATE for (const createCond of createConditions) { - parts.push(new Uint8Array(createCond.utxo)); - const amountBytes = bigintToLE(createCond.amount, 16); + parts.push(new Uint8Array(createCond.getUtxo())); + const amountBytes = bigintToLE(createCond.getAmount(), 16); parts.push(amountBytes); } // DEPOSIT for (const depositCond of depositConditions) { - parts.push(encoder.encode(depositCond.publicKey)); - parts.push(bigintToLE(depositCond.amount, 16)); + parts.push(encoder.encode(depositCond.getPublicKey())); + parts.push(bigintToLE(depositCond.getAmount(), 16)); } // WITHDRAW for (const withdrawCond of withdrawConditions) { - parts.push(encoder.encode(withdrawCond.publicKey)); - parts.push(bigintToLE(withdrawCond.amount, 16)); + parts.push(encoder.encode(withdrawCond.getPublicKey())); + parts.push(bigintToLE(withdrawCond.getAmount(), 16)); } const encodedLiveUntil = bigintToLE(BigInt(liveUntilLedger), 4); parts.push(encodedLiveUntil); // Concatenate all parts into one Uint8Array - const payloadBuffer = Buffer.concat(parts); const payload = Buffer.concat(parts); return payload; From 4f5df0cbb986dff7deedd7e6ad9acce268cb93ec Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Fri, 17 Oct 2025 10:46:51 -0300 Subject: [PATCH 40/90] test: fix transaction builder tests to use Condition class --- src/transaction-builder/index.unit.test.ts | 2209 +++++++++++--------- 1 file changed, 1229 insertions(+), 980 deletions(-) diff --git a/src/transaction-builder/index.unit.test.ts b/src/transaction-builder/index.unit.test.ts index 544eb80..b4f4b76 100644 --- a/src/transaction-builder/index.unit.test.ts +++ b/src/transaction-builder/index.unit.test.ts @@ -4,15 +4,24 @@ import { assertThrows, } from "https://deno.land/std@0.224.0/assert/mod.ts"; import { MoonlightTransactionBuilder } from "./index.ts"; -import { createOpToXDR, depositOpToXDR, withdrawOpToXDR, spendOpToXDR } from "./xdr/index.ts"; +import { + createOpToXDR, + depositOpToXDR, + withdrawOpToXDR, + spendOpToXDR, +} from "./xdr/index.ts"; import { Asset, Keypair, StrKey, xdr } from "@stellar/stellar-sdk"; import { Buffer } from "buffer"; -import { Condition } from "../conditions/types.ts"; +import { Condition } from "../conditions/index.ts"; import { StellarSmartContractId } from "../utils/types/stellar.types.ts"; // Mock data for testing -const mockChannelId: StellarSmartContractId = StrKey.encodeContract(Buffer.alloc(32)) as StellarSmartContractId; -const mockAuthId: StellarSmartContractId = StrKey.encodeContract(Buffer.alloc(32, 1)) as StellarSmartContractId; +const mockChannelId: StellarSmartContractId = StrKey.encodeContract( + Buffer.alloc(32) +) as StellarSmartContractId; +const mockAuthId: StellarSmartContractId = StrKey.encodeContract( + Buffer.alloc(32, 1) +) as StellarSmartContractId; const mockNetwork = "testnet"; const mockAsset = Asset.native(); @@ -25,23 +34,11 @@ const mockEd25519Key1 = Keypair.random().publicKey() as `G${string}`; const mockEd25519Key2 = Keypair.random().publicKey() as `G${string}`; // Mock conditions -const mockCreateCondition: Condition = { - action: "CREATE", - utxo: mockUTXO1, - amount: 1000n, -}; - -const mockDepositCondition: Condition = { - action: "DEPOSIT", - publicKey: mockEd25519Key1, - amount: 500n, -}; - -const mockWithdrawCondition: Condition = { - action: "WITHDRAW", - publicKey: mockEd25519Key2, - amount: 300n, -}; +const mockCreateCondition = Condition.create(mockUTXO1, 1000n); + +const mockDepositCondition = Condition.deposit(mockEd25519Key1, 500n); + +const mockWithdrawCondition = Condition.withdraw(mockEd25519Key2, 300n); // Helper function to create a test builder instance function createTestBuilder(): MoonlightTransactionBuilder { @@ -53,341 +50,405 @@ function createTestBuilder(): MoonlightTransactionBuilder { }); } -Deno.test("MoonlightTransactionBuilder - Basic Operations (Add Methods)", async (t) => { - await t.step("addCreate should add create operation with valid parameters", () => { - const builder = createTestBuilder(); - - const result = builder.addCreate(mockUTXO1, 1000n); - - // Should return builder instance for chaining - assertEquals(result, builder); - - // Should have added the create operation - const operations = builder.getOperation(); - assertEquals(operations.create.length, 1); - assertEquals(operations.create[0].utxo, mockUTXO1); - assertEquals(operations.create[0].amount, 1000n); - }); - - await t.step("addCreate should throw error when UTXO already exists", () => { - const builder = createTestBuilder(); - - builder.addCreate(mockUTXO1, 1000n); - - // Should throw error when adding same UTXO again - assertThrows( - () => builder.addCreate(mockUTXO1, 2000n), - Error, - "Create operation for this UTXO already exists" +Deno.test( + "MoonlightTransactionBuilder - Basic Operations (Add Methods)", + async (t) => { + await t.step( + "addCreate should add create operation with valid parameters", + () => { + const builder = createTestBuilder(); + + const result = builder.addCreate(mockUTXO1, 1000n); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Should have added the create operation + const operations = builder.getOperation(); + assertEquals(operations.create.length, 1); + assertEquals(operations.create[0].utxo, mockUTXO1); + assertEquals(operations.create[0].amount, 1000n); + } ); - }); - await t.step("addCreate should throw error when amount is zero or negative", () => { - const builder = createTestBuilder(); - - // Should throw error for zero amount - assertThrows( - () => builder.addCreate(mockUTXO1, 0n), - Error, - "Create operation amount must be positive" - ); - - // Should throw error for negative amount - assertThrows( - () => builder.addCreate(mockUTXO2, -100n), - Error, - "Create operation amount must be positive" - ); - }); + await t.step( + "addCreate should throw error when UTXO already exists", + () => { + const builder = createTestBuilder(); - await t.step("addSpend should add spend operation with valid parameters", () => { - const builder = createTestBuilder(); - const conditions = [mockCreateCondition, mockDepositCondition]; - - const result = builder.addSpend(mockUTXO1, conditions); - - // Should return builder instance for chaining - assertEquals(result, builder); - - // Should have added the spend operation - const operations = builder.getOperation(); - assertEquals(operations.spend.length, 1); - assertEquals(operations.spend[0].utxo, mockUTXO1); - assertEquals(operations.spend[0].conditions.length, 2); - assertEquals(operations.spend[0].conditions[0], mockCreateCondition); - assertEquals(operations.spend[0].conditions[1], mockDepositCondition); - }); + builder.addCreate(mockUTXO1, 1000n); - await t.step("addSpend should throw error when UTXO already exists", () => { - const builder = createTestBuilder(); - const conditions = [mockCreateCondition]; - - builder.addSpend(mockUTXO1, conditions); - - // Should throw error when adding same UTXO again - assertThrows( - () => builder.addSpend(mockUTXO1, [mockWithdrawCondition]), - Error, - "Spend operation for this UTXO already exists" + // Should throw error when adding same UTXO again + assertThrows( + () => builder.addCreate(mockUTXO1, 2000n), + Error, + "Create operation for this UTXO already exists" + ); + } ); - }); - - await t.step("addSpend should handle empty conditions array", () => { - const builder = createTestBuilder(); - - const result = builder.addSpend(mockUTXO1, []); - - // Should return builder instance for chaining - assertEquals(result, builder); - - // Should have added the spend operation with empty conditions - const operations = builder.getOperation(); - assertEquals(operations.spend.length, 1); - assertEquals(operations.spend[0].utxo, mockUTXO1); - assertEquals(operations.spend[0].conditions.length, 0); - }); - await t.step("addDeposit should add deposit operation with valid parameters", () => { - const builder = createTestBuilder(); - const conditions = [mockDepositCondition]; - - const result = builder.addDeposit(mockEd25519Key1, 500n, conditions); - - // Should return builder instance for chaining - assertEquals(result, builder); - - // Should have added the deposit operation - const operations = builder.getOperation(); - assertEquals(operations.deposit.length, 1); - assertEquals(operations.deposit[0].pubKey, mockEd25519Key1); - assertEquals(operations.deposit[0].amount, 500n); - assertEquals(operations.deposit[0].conditions.length, 1); - assertEquals(operations.deposit[0].conditions[0], mockDepositCondition); - }); + await t.step( + "addCreate should throw error when amount is zero or negative", + () => { + const builder = createTestBuilder(); + + // Should throw error for zero amount + assertThrows( + () => builder.addCreate(mockUTXO1, 0n), + Error, + "Create operation amount must be positive" + ); + + // Should throw error for negative amount + assertThrows( + () => builder.addCreate(mockUTXO2, -100n), + Error, + "Create operation amount must be positive" + ); + } + ); - await t.step("addDeposit should throw error when public key already exists", () => { - const builder = createTestBuilder(); - const conditions = [mockDepositCondition]; - - builder.addDeposit(mockEd25519Key1, 500n, conditions); - - // Should throw error when adding same public key again - assertThrows( - () => builder.addDeposit(mockEd25519Key1, 1000n, []), - Error, - "Deposit operation for this public key already exists" + await t.step( + "addSpend should add spend operation with valid parameters", + () => { + const builder = createTestBuilder(); + const conditions = [mockCreateCondition, mockDepositCondition]; + + const result = builder.addSpend(mockUTXO1, conditions); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Should have added the spend operation + const operations = builder.getOperation(); + assertEquals(operations.spend.length, 1); + assertEquals(operations.spend[0].utxo, mockUTXO1); + assertEquals(operations.spend[0].conditions.length, 2); + assertEquals(operations.spend[0].conditions[0], mockCreateCondition); + assertEquals(operations.spend[0].conditions[1], mockDepositCondition); + } ); - }); - await t.step("addDeposit should throw error when amount is zero or negative", () => { - const builder = createTestBuilder(); - const conditions = [mockDepositCondition]; - - // Should throw error for zero amount - assertThrows( - () => builder.addDeposit(mockEd25519Key1, 0n, conditions), - Error, - "Deposit operation amount must be positive" + await t.step("addSpend should throw error when UTXO already exists", () => { + const builder = createTestBuilder(); + const conditions = [mockCreateCondition]; + + builder.addSpend(mockUTXO1, conditions); + + // Should throw error when adding same UTXO again + assertThrows( + () => builder.addSpend(mockUTXO1, [mockWithdrawCondition]), + Error, + "Spend operation for this UTXO already exists" + ); + }); + + await t.step("addSpend should handle empty conditions array", () => { + const builder = createTestBuilder(); + + const result = builder.addSpend(mockUTXO1, []); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Should have added the spend operation with empty conditions + const operations = builder.getOperation(); + assertEquals(operations.spend.length, 1); + assertEquals(operations.spend[0].utxo, mockUTXO1); + assertEquals(operations.spend[0].conditions.length, 0); + }); + + await t.step( + "addDeposit should add deposit operation with valid parameters", + () => { + const builder = createTestBuilder(); + const conditions = [mockDepositCondition]; + + const result = builder.addDeposit(mockEd25519Key1, 500n, conditions); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Should have added the deposit operation + const operations = builder.getOperation(); + assertEquals(operations.deposit.length, 1); + assertEquals(operations.deposit[0].pubKey, mockEd25519Key1); + assertEquals(operations.deposit[0].amount, 500n); + assertEquals(operations.deposit[0].conditions.length, 1); + assertEquals(operations.deposit[0].conditions[0], mockDepositCondition); + } ); - - // Should throw error for negative amount - assertThrows( - () => builder.addDeposit(mockEd25519Key2, -100n, conditions), - Error, - "Deposit operation amount must be positive" + + await t.step( + "addDeposit should throw error when public key already exists", + () => { + const builder = createTestBuilder(); + const conditions = [mockDepositCondition]; + + builder.addDeposit(mockEd25519Key1, 500n, conditions); + + // Should throw error when adding same public key again + assertThrows( + () => builder.addDeposit(mockEd25519Key1, 1000n, []), + Error, + "Deposit operation for this public key already exists" + ); + } ); - }); - await t.step("addWithdraw should add withdraw operation with valid parameters", () => { - const builder = createTestBuilder(); - const conditions = [mockWithdrawCondition]; - - const result = builder.addWithdraw(mockEd25519Key1, 300n, conditions); - - // Should return builder instance for chaining - assertEquals(result, builder); - - // Should have added the withdraw operation - const operations = builder.getOperation(); - assertEquals(operations.withdraw.length, 1); - assertEquals(operations.withdraw[0].pubKey, mockEd25519Key1); - assertEquals(operations.withdraw[0].amount, 300n); - assertEquals(operations.withdraw[0].conditions.length, 1); - assertEquals(operations.withdraw[0].conditions[0], mockWithdrawCondition); - }); + await t.step( + "addDeposit should throw error when amount is zero or negative", + () => { + const builder = createTestBuilder(); + const conditions = [mockDepositCondition]; + + // Should throw error for zero amount + assertThrows( + () => builder.addDeposit(mockEd25519Key1, 0n, conditions), + Error, + "Deposit operation amount must be positive" + ); + + // Should throw error for negative amount + assertThrows( + () => builder.addDeposit(mockEd25519Key2, -100n, conditions), + Error, + "Deposit operation amount must be positive" + ); + } + ); - await t.step("addWithdraw should throw error when public key already exists", () => { - const builder = createTestBuilder(); - const conditions = [mockWithdrawCondition]; - - builder.addWithdraw(mockEd25519Key1, 300n, conditions); - - // Should throw error when adding same public key again - assertThrows( - () => builder.addWithdraw(mockEd25519Key1, 500n, []), - Error, - "Withdraw operation for this public key already exists" + await t.step( + "addWithdraw should add withdraw operation with valid parameters", + () => { + const builder = createTestBuilder(); + const conditions = [mockWithdrawCondition]; + + const result = builder.addWithdraw(mockEd25519Key1, 300n, conditions); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Should have added the withdraw operation + const operations = builder.getOperation(); + assertEquals(operations.withdraw.length, 1); + assertEquals(operations.withdraw[0].pubKey, mockEd25519Key1); + assertEquals(operations.withdraw[0].amount, 300n); + assertEquals(operations.withdraw[0].conditions.length, 1); + assertEquals( + operations.withdraw[0].conditions[0], + mockWithdrawCondition + ); + } ); - }); - await t.step("addWithdraw should throw error when amount is zero or negative", () => { - const builder = createTestBuilder(); - const conditions = [mockWithdrawCondition]; - - // Should throw error for zero amount - assertThrows( - () => builder.addWithdraw(mockEd25519Key1, 0n, conditions), - Error, - "Withdraw operation amount must be positive" + await t.step( + "addWithdraw should throw error when public key already exists", + () => { + const builder = createTestBuilder(); + const conditions = [mockWithdrawCondition]; + + builder.addWithdraw(mockEd25519Key1, 300n, conditions); + + // Should throw error when adding same public key again + assertThrows( + () => builder.addWithdraw(mockEd25519Key1, 500n, []), + Error, + "Withdraw operation for this public key already exists" + ); + } ); - - // Should throw error for negative amount - assertThrows( - () => builder.addWithdraw(mockEd25519Key2, -100n, conditions), - Error, - "Withdraw operation amount must be positive" + + await t.step( + "addWithdraw should throw error when amount is zero or negative", + () => { + const builder = createTestBuilder(); + const conditions = [mockWithdrawCondition]; + + // Should throw error for zero amount + assertThrows( + () => builder.addWithdraw(mockEd25519Key1, 0n, conditions), + Error, + "Withdraw operation amount must be positive" + ); + + // Should throw error for negative amount + assertThrows( + () => builder.addWithdraw(mockEd25519Key2, -100n, conditions), + Error, + "Withdraw operation amount must be positive" + ); + } ); - }); - await t.step("should allow chaining multiple operations", () => { - const builder = createTestBuilder(); - - const result = builder - .addCreate(mockUTXO1, 1000n) - .addCreate(mockUTXO2, 2000n) - .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]) - .addWithdraw(mockEd25519Key2, 300n, [mockWithdrawCondition]); - - // Should return builder instance - assertEquals(result, builder); - - // Should have all operations - const operations = builder.getOperation(); - assertEquals(operations.create.length, 2); - assertEquals(operations.spend.length, 1); - assertEquals(operations.deposit.length, 1); - assertEquals(operations.withdraw.length, 1); - }); -}); + await t.step("should allow chaining multiple operations", () => { + const builder = createTestBuilder(); + + const result = builder + .addCreate(mockUTXO1, 1000n) + .addCreate(mockUTXO2, 2000n) + .addSpend(mockUTXO1, [mockCreateCondition]) + .addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]) + .addWithdraw(mockEd25519Key2, 300n, [mockWithdrawCondition]); + + // Should return builder instance + assertEquals(result, builder); + + // Should have all operations + const operations = builder.getOperation(); + assertEquals(operations.create.length, 2); + assertEquals(operations.spend.length, 1); + assertEquals(operations.deposit.length, 1); + assertEquals(operations.withdraw.length, 1); + }); + } +); Deno.test("MoonlightTransactionBuilder - Internal Signatures", async (t) => { - await t.step("addInnerSignature should add signature for existing spend operation", () => { - const builder = createTestBuilder(); - const mockSignature = Buffer.alloc(64, 0x42); - const expirationLedger = 1000; - - // First add a spend operation - builder.addSpend(mockUTXO1, [mockCreateCondition]); - - const result = builder.addInnerSignature(mockUTXO1, mockSignature, expirationLedger); - - // Should return builder instance for chaining - assertEquals(result, builder); - - // Verify signature was added (we can't directly access private properties, - // but we can test that the method doesn't throw and returns the builder) - assertEquals(result instanceof MoonlightTransactionBuilder, true); - }); + await t.step( + "addInnerSignature should add signature for existing spend operation", + () => { + const builder = createTestBuilder(); + const mockSignature = Buffer.alloc(64, 0x42); + const expirationLedger = 1000; + + // First add a spend operation + builder.addSpend(mockUTXO1, [mockCreateCondition]); + + const result = builder.addInnerSignature( + mockUTXO1, + mockSignature, + expirationLedger + ); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Verify signature was added (we can't directly access private properties, + // but we can test that the method doesn't throw and returns the builder) + assertEquals(result instanceof MoonlightTransactionBuilder, true); + } + ); + + await t.step( + "addInnerSignature should throw error when UTXO not found in spend operations", + () => { + const builder = createTestBuilder(); + const mockSignature = Buffer.alloc(64, 0x42); + const expirationLedger = 1000; + + // Don't add any spend operations + + // Should throw error when trying to add signature for non-existent UTXO + assertThrows( + () => + builder.addInnerSignature(mockUTXO1, mockSignature, expirationLedger), + Error, + "No spend operation for this UTXO" + ); + } + ); + + await t.step( + "addProviderInnerSignature should add provider signature", + () => { + const builder = createTestBuilder(); + const mockSignature = Buffer.alloc(64, 0x43); + const expirationLedger = 1000; + const nonce = "123456789"; + + const result = builder.addProviderInnerSignature( + mockEd25519Key1, + mockSignature, + expirationLedger, + nonce + ); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Verify the method doesn't throw and returns the builder + assertEquals(result instanceof MoonlightTransactionBuilder, true); + } + ); - await t.step("addInnerSignature should throw error when UTXO not found in spend operations", () => { - const builder = createTestBuilder(); - const mockSignature = Buffer.alloc(64, 0x42); - const expirationLedger = 1000; - - // Don't add any spend operations - - // Should throw error when trying to add signature for non-existent UTXO - assertThrows( - () => builder.addInnerSignature(mockUTXO1, mockSignature, expirationLedger), - Error, - "No spend operation for this UTXO" - ); - }); + await t.step( + "addExtSignedEntry should add external signature for existing deposit", + () => { + const builder = createTestBuilder(); + const mockAuthEntry = {} as xdr.SorobanAuthorizationEntry; - await t.step("addProviderInnerSignature should add provider signature", () => { - const builder = createTestBuilder(); - const mockSignature = Buffer.alloc(64, 0x43); - const expirationLedger = 1000; - const nonce = "123456789"; - - const result = builder.addProviderInnerSignature( - mockEd25519Key1, - mockSignature, - expirationLedger, - nonce - ); - - // Should return builder instance for chaining - assertEquals(result, builder); - - // Verify the method doesn't throw and returns the builder - assertEquals(result instanceof MoonlightTransactionBuilder, true); - }); + // First add a deposit operation + builder.addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]); - await t.step("addExtSignedEntry should add external signature for existing deposit", () => { - const builder = createTestBuilder(); - const mockAuthEntry = {} as xdr.SorobanAuthorizationEntry; - - // First add a deposit operation - builder.addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]); - - const result = builder.addExtSignedEntry(mockEd25519Key1, mockAuthEntry); - - // Should return builder instance for chaining - assertEquals(result, builder); - - // Verify the method doesn't throw and returns the builder - assertEquals(result instanceof MoonlightTransactionBuilder, true); - }); + const result = builder.addExtSignedEntry(mockEd25519Key1, mockAuthEntry); - await t.step("addExtSignedEntry should add external signature for existing withdraw", () => { - const builder = createTestBuilder(); - const mockAuthEntry = {} as xdr.SorobanAuthorizationEntry; - - // First add a withdraw operation - builder.addWithdraw(mockEd25519Key1, 300n, [mockWithdrawCondition]); - - const result = builder.addExtSignedEntry(mockEd25519Key1, mockAuthEntry); - - // Should return builder instance for chaining - assertEquals(result, builder); - - // Verify the method doesn't throw and returns the builder - assertEquals(result instanceof MoonlightTransactionBuilder, true); - }); + // Should return builder instance for chaining + assertEquals(result, builder); - await t.step("addExtSignedEntry should throw error when public key not found", () => { - const builder = createTestBuilder(); - const mockAuthEntry = {} as xdr.SorobanAuthorizationEntry; - - // Don't add any deposit or withdraw operations - - // Should throw error when trying to add signature for non-existent public key - assertThrows( - () => builder.addExtSignedEntry(mockEd25519Key1, mockAuthEntry), - Error, - "No deposit or withdraw operation for this public key" - ); - }); + // Verify the method doesn't throw and returns the builder + assertEquals(result instanceof MoonlightTransactionBuilder, true); + } + ); + + await t.step( + "addExtSignedEntry should add external signature for existing withdraw", + () => { + const builder = createTestBuilder(); + const mockAuthEntry = {} as xdr.SorobanAuthorizationEntry; + + // First add a withdraw operation + builder.addWithdraw(mockEd25519Key1, 300n, [mockWithdrawCondition]); + + const result = builder.addExtSignedEntry(mockEd25519Key1, mockAuthEntry); + + // Should return builder instance for chaining + assertEquals(result, builder); + + // Verify the method doesn't throw and returns the builder + assertEquals(result instanceof MoonlightTransactionBuilder, true); + } + ); + + await t.step( + "addExtSignedEntry should throw error when public key not found", + () => { + const builder = createTestBuilder(); + const mockAuthEntry = {} as xdr.SorobanAuthorizationEntry; + + // Don't add any deposit or withdraw operations + + // Should throw error when trying to add signature for non-existent public key + assertThrows( + () => builder.addExtSignedEntry(mockEd25519Key1, mockAuthEntry), + Error, + "No deposit or withdraw operation for this public key" + ); + } + ); await t.step("should allow chaining signature operations", () => { const builder = createTestBuilder(); const mockSignature = Buffer.alloc(64, 0x44); const mockAuthEntry = {} as xdr.SorobanAuthorizationEntry; - + // Add operations first builder.addSpend(mockUTXO1, [mockCreateCondition]); builder.addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]); - + const result = builder .addInnerSignature(mockUTXO1, mockSignature, 1000) - .addProviderInnerSignature(mockEd25519Key1, mockSignature, 1000, "nonce123") + .addProviderInnerSignature( + mockEd25519Key1, + mockSignature, + 1000, + "nonce123" + ) .addExtSignedEntry(mockEd25519Key1, mockAuthEntry); - + // Should return builder instance assertEquals(result, builder); - + // Verify the method doesn't throw and returns the builder assertEquals(result instanceof MoonlightTransactionBuilder, true); }); @@ -396,49 +457,65 @@ Deno.test("MoonlightTransactionBuilder - Internal Signatures", async (t) => { const builder = createTestBuilder(); const mockSignature1 = Buffer.alloc(64, 0x45); const mockSignature2 = Buffer.alloc(64, 0x46); - - const result = builder - .addProviderInnerSignature(mockEd25519Key1, mockSignature1, 1000, "nonce1") - .addProviderInnerSignature(mockEd25519Key2, mockSignature2, 1000, "nonce2"); - - // Should return builder instance - assertEquals(result, builder); - - // Verify the method doesn't throw and returns the builder - assertEquals(result instanceof MoonlightTransactionBuilder, true); - }); - await t.step("should handle multiple inner signatures for different UTXOs", () => { - const builder = createTestBuilder(); - const mockSignature1 = Buffer.alloc(64, 0x47); - const mockSignature2 = Buffer.alloc(64, 0x48); - - // Add spend operations for different UTXOs - builder.addSpend(mockUTXO1, [mockCreateCondition]); - builder.addSpend(mockUTXO2, [mockDepositCondition]); - const result = builder - .addInnerSignature(mockUTXO1, mockSignature1, 1000) - .addInnerSignature(mockUTXO2, mockSignature2, 1000); - + .addProviderInnerSignature( + mockEd25519Key1, + mockSignature1, + 1000, + "nonce1" + ) + .addProviderInnerSignature( + mockEd25519Key2, + mockSignature2, + 1000, + "nonce2" + ); + // Should return builder instance assertEquals(result, builder); - + // Verify the method doesn't throw and returns the builder assertEquals(result instanceof MoonlightTransactionBuilder, true); }); + + await t.step( + "should handle multiple inner signatures for different UTXOs", + () => { + const builder = createTestBuilder(); + const mockSignature1 = Buffer.alloc(64, 0x47); + const mockSignature2 = Buffer.alloc(64, 0x48); + + // Add spend operations for different UTXOs + builder.addSpend(mockUTXO1, [mockCreateCondition]); + builder.addSpend(mockUTXO2, [mockDepositCondition]); + + const result = builder + .addInnerSignature(mockUTXO1, mockSignature1, 1000) + .addInnerSignature(mockUTXO2, mockSignature2, 1000); + + // Should return builder instance + assertEquals(result, builder); + + // Verify the method doesn't throw and returns the builder + assertEquals(result instanceof MoonlightTransactionBuilder, true); + } + ); }); Deno.test("MoonlightTransactionBuilder - Query Methods", async (t) => { - await t.step("getOperation should return empty arrays when no operations added", () => { - const builder = createTestBuilder(); - - const op = builder.getOperation(); - assertEquals(op.create.length, 0); - assertEquals(op.spend.length, 0); - assertEquals(op.deposit.length, 0); - assertEquals(op.withdraw.length, 0); - }); + await t.step( + "getOperation should return empty arrays when no operations added", + () => { + const builder = createTestBuilder(); + + const op = builder.getOperation(); + assertEquals(op.create.length, 0); + assertEquals(op.spend.length, 0); + assertEquals(op.deposit.length, 0); + assertEquals(op.withdraw.length, 0); + } + ); await t.step("getOperation should reflect added operations", () => { const builder = createTestBuilder(); @@ -466,122 +543,160 @@ Deno.test("MoonlightTransactionBuilder - Query Methods", async (t) => { assertEquals(dep?.conditions.length, 1); }); - await t.step("getDepositOperation should return undefined when not found", () => { - const builder = createTestBuilder(); - const dep = builder.getDepositOperation(mockEd25519Key2); - assertEquals(dep, undefined); - }); + await t.step( + "getDepositOperation should return undefined when not found", + () => { + const builder = createTestBuilder(); + const dep = builder.getDepositOperation(mockEd25519Key2); + assertEquals(dep, undefined); + } + ); }); -Deno.test("MoonlightTransactionBuilder - Authorization and Arguments", async (t) => { - await t.step("getExtAuthEntry should generate entry for existing deposit", () => { - const builder = createTestBuilder(); - builder.addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]); - - // Using deterministic values for validation - const nonce = "123"; - const exp = 456; - - const entry = builder.getExtAuthEntry(mockEd25519Key1, nonce, exp); - // We can't assert XDR internals without full mocks; ensure object exists - assertEquals(!!entry, true); - }); - - await t.step("getExtAuthEntry should throw when deposit is missing", () => { - const builder = createTestBuilder(); - const nonce = "123"; - const exp = 456; - - assertThrows( - () => builder.getExtAuthEntry(mockEd25519Key1, nonce, exp), - Error, - "No deposit operation for this address", +Deno.test( + "MoonlightTransactionBuilder - Authorization and Arguments", + async (t) => { + await t.step( + "getExtAuthEntry should generate entry for existing deposit", + () => { + const builder = createTestBuilder(); + builder.addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]); + + // Using deterministic values for validation + const nonce = "123"; + const exp = 456; + + const entry = builder.getExtAuthEntry(mockEd25519Key1, nonce, exp); + // We can't assert XDR internals without full mocks; ensure object exists + assertEquals(!!entry, true); + } ); - }); - await t.step("getAuthRequirementArgs should return empty when no spend", () => { - const builder = createTestBuilder(); - const args = builder.getAuthRequirementArgs(); - assertEquals(Array.isArray(args), true); - assertEquals(args.length, 0); - }); + await t.step("getExtAuthEntry should throw when deposit is missing", () => { + const builder = createTestBuilder(); + const nonce = "123"; + const exp = 456; + + assertThrows( + () => builder.getExtAuthEntry(mockEd25519Key1, nonce, exp), + Error, + "No deposit operation for this address" + ); + }); + + await t.step( + "getAuthRequirementArgs should return empty when no spend", + () => { + const builder = createTestBuilder(); + const args = builder.getAuthRequirementArgs(); + assertEquals(Array.isArray(args), true); + assertEquals(args.length, 0); + } + ); - await t.step("getAuthRequirementArgs should include ordered spend signers", () => { - const builder = createTestBuilder(); - // Add spend with two UTXOs in reverse order to verify ordering - builder - .addSpend(mockUTXO2, [mockDepositCondition]) - .addSpend(mockUTXO1, [mockCreateCondition]); - - const args = builder.getAuthRequirementArgs(); - // Expect one vector with one map of signers - assertEquals(args.length, 1); - // We can't deserialize xdr.ScVal here; presence suffices for unit test - assertEquals(!!args[0], true); - }); + await t.step( + "getAuthRequirementArgs should include ordered spend signers", + () => { + const builder = createTestBuilder(); + // Add spend with two UTXOs in reverse order to verify ordering + builder + .addSpend(mockUTXO2, [mockDepositCondition]) + .addSpend(mockUTXO1, [mockCreateCondition]); + + const args = builder.getAuthRequirementArgs(); + // Expect one vector with one map of signers + assertEquals(args.length, 1); + // We can't deserialize xdr.ScVal here; presence suffices for unit test + assertEquals(!!args[0], true); + } + ); - await t.step("getOperationAuthEntry should generate entry (unsigned)", () => { - const builder = createTestBuilder(); - // No spend: args should be empty, but entry is still generated - const entry = builder.getOperationAuthEntry("999", 1234, false); - assertEquals(!!entry, true); - }); -}); + await t.step( + "getOperationAuthEntry should generate entry (unsigned)", + () => { + const builder = createTestBuilder(); + // No spend: args should be empty, but entry is still generated + const entry = builder.getOperationAuthEntry("999", 1234, false); + assertEquals(!!entry, true); + } + ); + } +); Deno.test("MoonlightTransactionBuilder - Hash and Signature XDR", async (t) => { - await t.step("getOperationAuthEntryHash should return hash for given parameters", async () => { - const builder = createTestBuilder(); - const nonce = "123456789"; - const exp = 1000; - - const hash = await builder.getOperationAuthEntryHash(nonce, exp); - // Should return a 32-byte hash - assertEquals(hash.length, 32); - assertEquals(hash instanceof Uint8Array, true); - }); - - await t.step("getOperationAuthEntryHash should use network ID correctly", async () => { - const builder = createTestBuilder(); - const nonce = "123456789"; - const exp = 1000; - - const hash1 = await builder.getOperationAuthEntryHash(nonce, exp); - const hash2 = await builder.getOperationAuthEntryHash(nonce, exp); - // Same parameters should produce same hash - assertEquals(hash1, hash2); - }); - - await t.step("getOperationAuthEntryHash should handle different nonce values", async () => { - const builder = createTestBuilder(); - const exp = 1000; - - const hash1 = await builder.getOperationAuthEntryHash("123456789", exp); - const hash2 = await builder.getOperationAuthEntryHash("987654321", exp); - // Different nonces should produce different hashes - assertEquals(hash1.length, 32); - assertEquals(hash2.length, 32); - // Hashes should be different - assertEquals(hash1.every((byte, i) => byte === hash2[i]), false); - }); - - await t.step("signaturesXDR should throw error when no provider signatures", () => { - const builder = createTestBuilder(); - // Add spend operation but no provider signature - builder.addSpend(mockUTXO1, [mockCreateCondition]); - - assertThrows( - () => builder.signaturesXDR(), - Error, - "No Provider signatures added", - ); - }); + await t.step( + "getOperationAuthEntryHash should return hash for given parameters", + async () => { + const builder = createTestBuilder(); + const nonce = "123456789"; + const exp = 1000; + + const hash = await builder.getOperationAuthEntryHash(nonce, exp); + // Should return a 32-byte hash + assertEquals(hash.length, 32); + assertEquals(hash instanceof Uint8Array, true); + } + ); + + await t.step( + "getOperationAuthEntryHash should use network ID correctly", + async () => { + const builder = createTestBuilder(); + const nonce = "123456789"; + const exp = 1000; + + const hash1 = await builder.getOperationAuthEntryHash(nonce, exp); + const hash2 = await builder.getOperationAuthEntryHash(nonce, exp); + // Same parameters should produce same hash + assertEquals(hash1, hash2); + } + ); + + await t.step( + "getOperationAuthEntryHash should handle different nonce values", + async () => { + const builder = createTestBuilder(); + const exp = 1000; + + const hash1 = await builder.getOperationAuthEntryHash("123456789", exp); + const hash2 = await builder.getOperationAuthEntryHash("987654321", exp); + // Different nonces should produce different hashes + assertEquals(hash1.length, 32); + assertEquals(hash2.length, 32); + // Hashes should be different + assertEquals( + hash1.every((byte, i) => byte === hash2[i]), + false + ); + } + ); + + await t.step( + "signaturesXDR should throw error when no provider signatures", + () => { + const builder = createTestBuilder(); + // Add spend operation but no provider signature + builder.addSpend(mockUTXO1, [mockCreateCondition]); + + assertThrows( + () => builder.signaturesXDR(), + Error, + "No Provider signatures added" + ); + } + ); await t.step("signaturesXDR should return correct XDR format", () => { const builder = createTestBuilder(); const mockSignature = Buffer.alloc(64, 0x42); // Add provider signature - builder.addProviderInnerSignature(mockEd25519Key1, mockSignature, 1000, "nonce123"); + builder.addProviderInnerSignature( + mockEd25519Key1, + mockSignature, + 1000, + "nonce123" + ); const xdrString = builder.signaturesXDR(); // Should return a base64 XDR string @@ -596,8 +711,18 @@ Deno.test("MoonlightTransactionBuilder - Hash and Signature XDR", async (t) => { // Add provider signatures in reverse order builder - .addProviderInnerSignature(mockEd25519Key2, mockSignature2, 1000, "nonce2") - .addProviderInnerSignature(mockEd25519Key1, mockSignature1, 1000, "nonce1"); + .addProviderInnerSignature( + mockEd25519Key2, + mockSignature2, + 1000, + "nonce2" + ) + .addProviderInnerSignature( + mockEd25519Key1, + mockSignature1, + 1000, + "nonce1" + ); const xdrString = builder.signaturesXDR(); // Should return valid XDR string (ordering is internal, we just verify it works) @@ -605,189 +730,231 @@ Deno.test("MoonlightTransactionBuilder - Hash and Signature XDR", async (t) => { assertEquals(xdrString.length > 0, true); }); - await t.step("signaturesXDR should handle both provider and spend signatures", () => { - const builder = createTestBuilder(); - const mockSignature = Buffer.alloc(64, 0x44); - - // Add spend operation and signatures - builder - .addSpend(mockUTXO1, [mockCreateCondition]) - .addInnerSignature(mockUTXO1, mockSignature, 1000) - .addProviderInnerSignature(mockEd25519Key1, mockSignature, 1000, "nonce123"); - - const xdrString = builder.signaturesXDR(); - // Should return valid XDR string with both types - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - }); -}); - -Deno.test("MoonlightTransactionBuilder - High-Level Signing Methods", async (t) => { - await t.step("signWithProvider should sign with provided keypair", async () => { - const builder = createTestBuilder(); - const keypair = Keypair.random(); - const expirationLedger = 1000; - - await builder.signWithProvider(keypair, expirationLedger); - - // Verify that provider signature was added by checking signaturesXDR doesn't throw - const xdrString = builder.signaturesXDR(); - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - }); - - await t.step("signWithProvider should use provided nonce", async () => { - const builder = createTestBuilder(); - const keypair = Keypair.random(); - const expirationLedger = 1000; - const customNonce = "999888777"; - - await builder.signWithProvider(keypair, expirationLedger, customNonce); - - // Should not throw and should generate valid XDR - const xdrString = builder.signaturesXDR(); - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - }); - - await t.step("signWithSpendUtxo should throw error when UTXO not found", async () => { - const builder = createTestBuilder(); - const mockUtxo = { - publicKey: mockUTXO1, - privateKey: Buffer.alloc(32, 0x01), - signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42) - }; - const expirationLedger = 1000; + await t.step( + "signaturesXDR should handle both provider and spend signatures", + () => { + const builder = createTestBuilder(); + const mockSignature = Buffer.alloc(64, 0x44); - let errorThrown = false; - try { - await builder.signWithSpendUtxo(mockUtxo, expirationLedger); - } catch (error) { - errorThrown = true; - assertEquals((error as Error).message, "No spend operation for this UTXO"); + // Add spend operation and signatures + builder + .addSpend(mockUTXO1, [mockCreateCondition]) + .addInnerSignature(mockUTXO1, mockSignature, 1000) + .addProviderInnerSignature( + mockEd25519Key1, + mockSignature, + 1000, + "nonce123" + ); + + const xdrString = builder.signaturesXDR(); + // Should return valid XDR string with both types + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); } - assertEquals(errorThrown, true); - }); - - await t.step("signWithSpendUtxo should sign with UTXO keypair when found", async () => { - const builder = createTestBuilder(); - const mockUtxo = { - publicKey: mockUTXO1, - privateKey: Buffer.alloc(32, 0x01), - signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42) - }; - const expirationLedger = 1000; - - // Add spend operation first - builder.addSpend(mockUTXO1, [mockCreateCondition]); - - await builder.signWithSpendUtxo(mockUtxo, expirationLedger); - - // Should not throw - signature was added - assertEquals(true, true); // Test passes if no exception - }); - - await t.step("signExtWithEd25519 should sign external auth entry", async () => { - const builder = createTestBuilder(); - const keypair = Keypair.random(); - const expirationLedger = 1000; - - // Add deposit operation first - builder.addDeposit(keypair.publicKey() as `G${string}`, 500n, [mockDepositCondition]); + ); +}); - await builder.signExtWithEd25519(keypair, expirationLedger); +Deno.test( + "MoonlightTransactionBuilder - High-Level Signing Methods", + async (t) => { + await t.step( + "signWithProvider should sign with provided keypair", + async () => { + const builder = createTestBuilder(); + const keypair = Keypair.random(); + const expirationLedger = 1000; + + await builder.signWithProvider(keypair, expirationLedger); + + // Verify that provider signature was added by checking signaturesXDR doesn't throw + const xdrString = builder.signaturesXDR(); + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + } + ); - // Should not throw - external signature was added - assertEquals(true, true); // Test passes if no exception - }); + await t.step("signWithProvider should use provided nonce", async () => { + const builder = createTestBuilder(); + const keypair = Keypair.random(); + const expirationLedger = 1000; + const customNonce = "999888777"; + + await builder.signWithProvider(keypair, expirationLedger, customNonce); + + // Should not throw and should generate valid XDR + const xdrString = builder.signaturesXDR(); + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + }); + + await t.step( + "signWithSpendUtxo should throw error when UTXO not found", + async () => { + const builder = createTestBuilder(); + const mockUtxo = { + publicKey: mockUTXO1, + privateKey: Buffer.alloc(32, 0x01), + signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42), + }; + const expirationLedger = 1000; + + let errorThrown = false; + try { + await builder.signWithSpendUtxo(mockUtxo, expirationLedger); + } catch (error) { + errorThrown = true; + assertEquals( + (error as Error).message, + "No spend operation for this UTXO" + ); + } + assertEquals(errorThrown, true); + } + ); - await t.step("signExtWithEd25519 should use provided nonce", async () => { - const builder = createTestBuilder(); - const keypair = Keypair.random(); - const expirationLedger = 1000; - const customNonce = "555444333"; + await t.step( + "signWithSpendUtxo should sign with UTXO keypair when found", + async () => { + const builder = createTestBuilder(); + const mockUtxo = { + publicKey: mockUTXO1, + privateKey: Buffer.alloc(32, 0x01), + signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42), + }; + const expirationLedger = 1000; + + // Add spend operation first + builder.addSpend(mockUTXO1, [mockCreateCondition]); + + await builder.signWithSpendUtxo(mockUtxo, expirationLedger); + + // Should not throw - signature was added + assertEquals(true, true); // Test passes if no exception + } + ); - // Add deposit operation first - builder.addDeposit(keypair.publicKey() as `G${string}`, 500n, [mockDepositCondition]); + await t.step( + "signExtWithEd25519 should sign external auth entry", + async () => { + const builder = createTestBuilder(); + const keypair = Keypair.random(); + const expirationLedger = 1000; - await builder.signExtWithEd25519(keypair, expirationLedger, customNonce); + // Add deposit operation first + builder.addDeposit(keypair.publicKey() as `G${string}`, 500n, [ + mockDepositCondition, + ]); - // Should not throw - custom nonce was used and signature added - assertEquals(true, true); // Test passes if no exception - }); + await builder.signExtWithEd25519(keypair, expirationLedger); - await t.step("should handle complex signing workflow", async () => { - const builder = createTestBuilder(); - const providerKeypair = Keypair.random(); - const userKeypair = Keypair.random(); - const mockUtxo = { - publicKey: mockUTXO1, - privateKey: Buffer.alloc(32, 0x01), - signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42) - }; - const expirationLedger = 1000; + // Should not throw - external signature was added + assertEquals(true, true); // Test passes if no exception + } + ); - // Add operations - builder - .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [mockDepositCondition]); + await t.step("signExtWithEd25519 should use provided nonce", async () => { + const builder = createTestBuilder(); + const keypair = Keypair.random(); + const expirationLedger = 1000; + const customNonce = "555444333"; + + // Add deposit operation first + builder.addDeposit(keypair.publicKey() as `G${string}`, 500n, [ + mockDepositCondition, + ]); + + await builder.signExtWithEd25519(keypair, expirationLedger, customNonce); + + // Should not throw - custom nonce was used and signature added + assertEquals(true, true); // Test passes if no exception + }); + + await t.step("should handle complex signing workflow", async () => { + const builder = createTestBuilder(); + const providerKeypair = Keypair.random(); + const userKeypair = Keypair.random(); + const mockUtxo = { + publicKey: mockUTXO1, + privateKey: Buffer.alloc(32, 0x01), + signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42), + }; + const expirationLedger = 1000; + + // Add operations + builder + .addSpend(mockUTXO1, [mockCreateCondition]) + .addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [ + mockDepositCondition, + ]); - // Sign with all methods (now that buildAuthPayloadHash is implemented) - await builder.signWithProvider(providerKeypair, expirationLedger); - await builder.signWithSpendUtxo(mockUtxo, expirationLedger); - await builder.signExtWithEd25519(userKeypair, expirationLedger); + // Sign with all methods (now that buildAuthPayloadHash is implemented) + await builder.signWithProvider(providerKeypair, expirationLedger); + await builder.signWithSpendUtxo(mockUtxo, expirationLedger); + await builder.signExtWithEd25519(userKeypair, expirationLedger); - // Should generate valid XDR with all signatures - const xdrString = builder.signaturesXDR(); - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - }); -}); + // Should generate valid XDR with all signatures + const xdrString = builder.signaturesXDR(); + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + }); + } +); Deno.test("MoonlightTransactionBuilder - Final Methods", async (t) => { - await t.step("getSignedAuthEntries should return all signed entries", async () => { - const builder = createTestBuilder(); - const providerKeypair = Keypair.random(); - const userKeypair = Keypair.random(); - const expirationLedger = 1000; - - // Add operations and sign - builder.addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [mockDepositCondition]); - await builder.signWithProvider(providerKeypair, expirationLedger); - await builder.signExtWithEd25519(userKeypair, expirationLedger); - - const signedEntries = builder.getSignedAuthEntries(); - - // Should return an array of signed auth entries - assertEquals(Array.isArray(signedEntries), true); - assertEquals(signedEntries.length, 2); // External + operation entry - }); - - await t.step("getSignedAuthEntries should include external and operation entries", async () => { - const builder = createTestBuilder(); - const providerKeypair = Keypair.random(); - const userKeypair = Keypair.random(); - const expirationLedger = 1000; - - // Add operations and sign - builder.addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [mockDepositCondition]); - await builder.signWithProvider(providerKeypair, expirationLedger); - await builder.signExtWithEd25519(userKeypair, expirationLedger); - - const signedEntries = builder.getSignedAuthEntries(); - - // Should have both external and operation entries - assertEquals(signedEntries.length >= 2, true); - - // Each entry should be a valid SorobanAuthorizationEntry - for (const entry of signedEntries) { - assertEquals(!!entry, true); + await t.step( + "getSignedAuthEntries should return all signed entries", + async () => { + const builder = createTestBuilder(); + const providerKeypair = Keypair.random(); + const userKeypair = Keypair.random(); + const expirationLedger = 1000; + + // Add operations and sign + builder.addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [ + mockDepositCondition, + ]); + await builder.signWithProvider(providerKeypair, expirationLedger); + await builder.signExtWithEd25519(userKeypair, expirationLedger); + + const signedEntries = builder.getSignedAuthEntries(); + + // Should return an array of signed auth entries + assertEquals(Array.isArray(signedEntries), true); + assertEquals(signedEntries.length, 2); // External + operation entry } - }); + ); + + await t.step( + "getSignedAuthEntries should include external and operation entries", + async () => { + const builder = createTestBuilder(); + const providerKeypair = Keypair.random(); + const userKeypair = Keypair.random(); + const expirationLedger = 1000; + + // Add operations and sign + builder.addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [ + mockDepositCondition, + ]); + await builder.signWithProvider(providerKeypair, expirationLedger); + await builder.signExtWithEd25519(userKeypair, expirationLedger); + + const signedEntries = builder.getSignedAuthEntries(); + + // Should have both external and operation entries + assertEquals(signedEntries.length >= 2, true); + + // Each entry should be a valid SorobanAuthorizationEntry + for (const entry of signedEntries) { + assertEquals(!!entry, true); + } + } + ); await t.step("buildXDR should include all operation types", () => { const builder = createTestBuilder(); - + // Add one of each operation type builder .addCreate(mockUTXO1, 1000n) @@ -796,24 +963,24 @@ Deno.test("MoonlightTransactionBuilder - Final Methods", async (t) => { .addWithdraw(mockEd25519Key2, 300n, [mockWithdrawCondition]); const xdr = builder.buildXDR(); - + // Should return valid XDR structure assertEquals(!!xdr, true); }); await t.step("buildXDR should handle empty operations correctly", () => { const builder = createTestBuilder(); - + // Don't add any operations const xdr = builder.buildXDR(); - + // Should still return valid XDR structure with empty arrays assertEquals(!!xdr, true); }); await t.step("buildXDR should handle mixed operations", () => { const builder = createTestBuilder(); - + // Add multiple operations of different types builder .addCreate(mockUTXO1, 1000n) @@ -824,7 +991,7 @@ Deno.test("MoonlightTransactionBuilder - Final Methods", async (t) => { .addWithdraw(mockEd25519Key1, 200n, [mockWithdrawCondition]); const xdr = builder.buildXDR(); - + // Should return valid XDR structure assertEquals(!!xdr, true); }); @@ -836,7 +1003,7 @@ Deno.test("MoonlightTransactionBuilder - Final Methods", async (t) => { const mockUtxo = { publicKey: mockUTXO1, privateKey: Buffer.alloc(32, 0x01), - signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42) + signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42), }; const expirationLedger = 1000; @@ -844,8 +1011,12 @@ Deno.test("MoonlightTransactionBuilder - Final Methods", async (t) => { builder .addCreate(mockUTXO1, 1000n) .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [mockDepositCondition]) - .addWithdraw(userKeypair.publicKey() as `G${string}`, 200n, [mockWithdrawCondition]); + .addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [ + mockDepositCondition, + ]) + .addWithdraw(userKeypair.publicKey() as `G${string}`, 200n, [ + mockWithdrawCondition, + ]); // Sign with all methods await builder.signWithProvider(providerKeypair, expirationLedger); @@ -867,90 +1038,102 @@ Deno.test("MoonlightTransactionBuilder - Final Methods", async (t) => { }); Deno.test("Transaction Builder Utility Functions", async (t) => { - await t.step("createOpToXDR should convert create operation to XDR correctly", () => { - const createOp = { - utxo: mockUTXO1, - amount: 1000n - }; + await t.step( + "createOpToXDR should convert create operation to XDR correctly", + () => { + const createOp = { + utxo: mockUTXO1, + amount: 1000n, + }; + + const xdr = createOpToXDR(createOp); + + // Should return a valid ScVal + assertEquals(!!xdr, true); + } + ); - const xdr = createOpToXDR(createOp); - - // Should return a valid ScVal - assertEquals(!!xdr, true); - }); + await t.step( + "depositOpToXDR should convert deposit operation to XDR correctly", + () => { + const depositOp = { + pubKey: mockEd25519Key1, + amount: 500n, + conditions: [mockDepositCondition], + }; - await t.step("depositOpToXDR should convert deposit operation to XDR correctly", () => { - const depositOp = { - pubKey: mockEd25519Key1, - amount: 500n, - conditions: [mockDepositCondition] - }; + const xdr = depositOpToXDR(depositOp); - const xdr = depositOpToXDR(depositOp); - - // Should return a valid ScVal - assertEquals(!!xdr, true); - }); + // Should return a valid ScVal + assertEquals(!!xdr, true); + } + ); await t.step("depositOpToXDR should handle empty conditions", () => { const depositOp = { pubKey: mockEd25519Key1, amount: 500n, - conditions: [] + conditions: [], }; const xdr = depositOpToXDR(depositOp); - + // Should return a valid ScVal even with empty conditions assertEquals(!!xdr, true); }); - await t.step("withdrawOpToXDR should convert withdraw operation to XDR correctly", () => { - const withdrawOp = { - pubKey: mockEd25519Key1, - amount: 300n, - conditions: [mockWithdrawCondition] - }; + await t.step( + "withdrawOpToXDR should convert withdraw operation to XDR correctly", + () => { + const withdrawOp = { + pubKey: mockEd25519Key1, + amount: 300n, + conditions: [mockWithdrawCondition], + }; - const xdr = withdrawOpToXDR(withdrawOp); - - // Should return a valid ScVal - assertEquals(!!xdr, true); - }); + const xdr = withdrawOpToXDR(withdrawOp); + + // Should return a valid ScVal + assertEquals(!!xdr, true); + } + ); await t.step("withdrawOpToXDR should handle empty conditions", () => { const withdrawOp = { pubKey: mockEd25519Key1, amount: 300n, - conditions: [] + conditions: [], }; const xdr = withdrawOpToXDR(withdrawOp); - + // Should return a valid ScVal even with empty conditions assertEquals(!!xdr, true); }); - await t.step("spendOpToXDR should convert spend operation to XDR correctly", () => { - const spendOp = { - utxo: mockUTXO1, - conditions: [mockCreateCondition, mockDepositCondition] - }; + await t.step( + "spendOpToXDR should convert spend operation to XDR correctly", + () => { + const spendOp = { + utxo: mockUTXO1, + conditions: [mockCreateCondition, mockDepositCondition], + }; - const xdr = spendOpToXDR(spendOp); - - // Should return a valid ScVal - assertEquals(!!xdr, true); - }); + const xdr = spendOpToXDR(spendOp); + + // Should return a valid ScVal + assertEquals(!!xdr, true); + } + ); await t.step("spendOpToXDR should handle empty conditions", () => { const spendOp = { utxo: mockUTXO1, - conditions: [] + conditions: [], }; const xdr = spendOpToXDR(spendOp); - + // Should return a valid ScVal even with empty conditions assertEquals(!!xdr, true); }); @@ -959,53 +1142,65 @@ Deno.test("Transaction Builder Utility Functions", async (t) => { // Test createOpToXDR with different amounts const createOp1 = { utxo: mockUTXO1, amount: 1n }; const createOp2 = { utxo: mockUTXO2, amount: 999999999n }; - + const xdr1 = createOpToXDR(createOp1); const xdr2 = createOpToXDR(createOp2); - + assertEquals(!!xdr1, true); assertEquals(!!xdr2, true); // Test depositOpToXDR with different amounts const depositOp1 = { pubKey: mockEd25519Key1, amount: 1n, conditions: [] }; - const depositOp2 = { pubKey: mockEd25519Key2, amount: 999999999n, conditions: [] }; - + const depositOp2 = { + pubKey: mockEd25519Key2, + amount: 999999999n, + conditions: [], + }; + const xdr3 = depositOpToXDR(depositOp1); const xdr4 = depositOpToXDR(depositOp2); - + assertEquals(!!xdr3, true); assertEquals(!!xdr4, true); // Test withdrawOpToXDR with different amounts const withdrawOp1 = { pubKey: mockEd25519Key1, amount: 1n, conditions: [] }; - const withdrawOp2 = { pubKey: mockEd25519Key2, amount: 999999999n, conditions: [] }; - + const withdrawOp2 = { + pubKey: mockEd25519Key2, + amount: 999999999n, + conditions: [], + }; + const xdr5 = withdrawOpToXDR(withdrawOp1); const xdr6 = withdrawOpToXDR(withdrawOp2); - + assertEquals(!!xdr5, true); assertEquals(!!xdr6, true); }); await t.step("utility functions should handle multiple conditions", () => { // Test with multiple conditions - const multipleConditions = [mockCreateCondition, mockDepositCondition, mockWithdrawCondition]; - + const multipleConditions = [ + mockCreateCondition, + mockDepositCondition, + mockWithdrawCondition, + ]; + const depositOp = { pubKey: mockEd25519Key1, amount: 500n, - conditions: multipleConditions + conditions: multipleConditions, }; const withdrawOp = { pubKey: mockEd25519Key2, amount: 300n, - conditions: multipleConditions + conditions: multipleConditions, }; const spendOp = { utxo: mockUTXO1, - conditions: multipleConditions + conditions: multipleConditions, }; const depositXdr = depositOpToXDR(depositOp); @@ -1018,300 +1213,354 @@ Deno.test("Transaction Builder Utility Functions", async (t) => { }); }); -Deno.test("MoonlightTransactionBuilder - Integration and Edge Cases", async (t) => { - await t.step("should build complete transaction with all operation types", async () => { - const builder = createTestBuilder(); - const providerKeypair = Keypair.random(); - const userKeypair1 = Keypair.random(); - const userKeypair2 = Keypair.random(); - const mockUtxo1 = { - publicKey: mockUTXO1, - privateKey: Buffer.alloc(32, 0x01), - signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42) - }; - const mockUtxo2 = { - publicKey: mockUTXO2, - privateKey: Buffer.alloc(32, 0x02), - signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x43) - }; - const expirationLedger = 1000; - - // Add all types of operations - builder - .addCreate(mockUTXO1, 1000n) - .addCreate(mockUTXO2, 2000n) - .addSpend(mockUTXO1, [mockCreateCondition]) - .addSpend(mockUTXO2, [mockDepositCondition, mockWithdrawCondition]) - .addDeposit(userKeypair1.publicKey() as `G${string}`, 500n, [mockDepositCondition]) - .addDeposit(userKeypair2.publicKey() as `G${string}`, 300n, [mockWithdrawCondition]) - .addWithdraw(userKeypair1.publicKey() as `G${string}`, 200n, [mockWithdrawCondition]) - .addWithdraw(userKeypair2.publicKey() as `G${string}`, 100n, [mockCreateCondition]); - - // Sign with all methods - await builder.signWithProvider(providerKeypair, expirationLedger); - await builder.signWithSpendUtxo(mockUtxo1, expirationLedger); - await builder.signWithSpendUtxo(mockUtxo2, expirationLedger); - await builder.signExtWithEd25519(userKeypair1, expirationLedger); - await builder.signExtWithEd25519(userKeypair2, expirationLedger); - - // Verify all components work together - const operations = builder.getOperation(); - const signedEntries = builder.getSignedAuthEntries(); - const xdr = builder.buildXDR(); - const xdrString = builder.signaturesXDR(); - - // Validate operations - assertEquals(operations.create.length, 2); - assertEquals(operations.spend.length, 2); - assertEquals(operations.deposit.length, 2); - assertEquals(operations.withdraw.length, 2); - - // Validate signatures - assertEquals(Array.isArray(signedEntries), true); - assertEquals(signedEntries.length >= 3, true); // Provider + 2 external - - // Validate XDR - assertEquals(!!xdr, true); - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - }); - - await t.step("should handle complex transaction with multiple signatures", async () => { - const builder = createTestBuilder(); - const providerKeypair1 = Keypair.random(); - const providerKeypair2 = Keypair.random(); - const userKeypair1 = Keypair.random(); - const userKeypair2 = Keypair.random(); - const userKeypair3 = Keypair.random(); - const expirationLedger = 1000; - - // Add operations - each user keypair needs both deposit and withdraw operations - builder - .addCreate(mockUTXO1, 1000n) - .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(userKeypair1.publicKey() as `G${string}`, 500n, [mockDepositCondition]) - .addDeposit(userKeypair2.publicKey() as `G${string}`, 300n, [mockWithdrawCondition]) - .addDeposit(userKeypair3.publicKey() as `G${string}`, 200n, [mockWithdrawCondition]) - .addWithdraw(userKeypair1.publicKey() as `G${string}`, 200n, [mockWithdrawCondition]) - .addWithdraw(userKeypair2.publicKey() as `G${string}`, 100n, [mockCreateCondition]) - .addWithdraw(userKeypair3.publicKey() as `G${string}`, 150n, [mockDepositCondition]); - - // Add multiple provider signatures - await builder.signWithProvider(providerKeypair1, expirationLedger); - await builder.signWithProvider(providerKeypair2, expirationLedger); - - // Add multiple external signatures (each keypair has both deposit and withdraw) - await builder.signExtWithEd25519(userKeypair1, expirationLedger); - await builder.signExtWithEd25519(userKeypair2, expirationLedger); - await builder.signExtWithEd25519(userKeypair3, expirationLedger); - - // Verify multiple signatures are handled correctly - const signedEntries = builder.getSignedAuthEntries(); - const xdrString = builder.signaturesXDR(); - - assertEquals(Array.isArray(signedEntries), true); - assertEquals(signedEntries.length >= 4, true); // 2 providers + 3 external - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - }); - - await t.step("should validate transaction integrity", async () => { - const builder = createTestBuilder(); - const providerKeypair = Keypair.random(); - const userKeypair = Keypair.random(); - const mockUtxo = { - publicKey: mockUTXO1, - privateKey: Buffer.alloc(32, 0x01), - signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42) - }; - const expirationLedger = 1000; - - // Build transaction - builder - .addCreate(mockUTXO1, 1000n) - .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [mockDepositCondition]); - - await builder.signWithProvider(providerKeypair, expirationLedger); - await builder.signWithSpendUtxo(mockUtxo, expirationLedger); - await builder.signExtWithEd25519(userKeypair, expirationLedger); - - // Verify transaction integrity - const operations = builder.getOperation(); - const signedEntries = builder.getSignedAuthEntries(); - const xdr = builder.buildXDR(); - const xdrString = builder.signaturesXDR(); - - // All components should be consistent - assertEquals(operations.create.length, 1); - assertEquals(operations.spend.length, 1); - assertEquals(operations.deposit.length, 1); - assertEquals(operations.withdraw.length, 0); - - assertEquals(Array.isArray(signedEntries), true); - assertEquals(signedEntries.length >= 2, true); - assertEquals(!!xdr, true); - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - }); - - await t.step("should handle maximum number of operations", () => { - const builder = createTestBuilder(); - const maxOperations = 10; - - // Add maximum number of each operation type - for (let i = 0; i < maxOperations; i++) { - const utxo = new Uint8Array([i, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6, i + 7]); - const keypair = Keypair.random(); - - builder - .addCreate(utxo, BigInt(1000 + i)) - .addSpend(utxo, [mockCreateCondition]) - .addDeposit(keypair.publicKey() as `G${string}`, BigInt(500 + i), [mockDepositCondition]) - .addWithdraw(keypair.publicKey() as `G${string}`, BigInt(300 + i), [mockWithdrawCondition]); - } - - const operations = builder.getOperation(); - const xdr = builder.buildXDR(); - - assertEquals(operations.create.length, maxOperations); - assertEquals(operations.spend.length, maxOperations); - assertEquals(operations.deposit.length, maxOperations); - assertEquals(operations.withdraw.length, maxOperations); - assertEquals(!!xdr, true); - }); - - await t.step("should handle edge cases with zero amounts", () => { - const builder = createTestBuilder(); - - // These should throw errors for zero amounts - assertThrows( - () => builder.addCreate(mockUTXO1, 0n), - Error, - "Create operation amount must be positive" - ); - - assertThrows( - () => builder.addDeposit(mockEd25519Key1, 0n, []), - Error, - "Deposit operation amount must be positive" - ); - - assertThrows( - () => builder.addWithdraw(mockEd25519Key1, 0n, []), - Error, - "Withdraw operation amount must be positive" - ); - }); - - await t.step("should handle edge cases with negative amounts", () => { - const builder = createTestBuilder(); - - // These should throw errors for negative amounts - assertThrows( - () => builder.addCreate(mockUTXO1, -100n), - Error, - "Create operation amount must be positive" - ); - - assertThrows( - () => builder.addDeposit(mockEd25519Key1, -100n, []), - Error, - "Deposit operation amount must be positive" - ); - - assertThrows( - () => builder.addWithdraw(mockEd25519Key1, -100n, []), - Error, - "Withdraw operation amount must be positive" - ); - }); - - await t.step("should handle invalid input parameters", () => { - const builder = createTestBuilder(); - - // Test with empty UTXO array - should work but be a valid UTXO - const emptyUtxo = new Uint8Array(8).fill(0); - builder.addCreate(emptyUtxo, 1000n); - - // Try to add the same UTXO again - should throw error - assertThrows( - () => builder.addCreate(emptyUtxo, 2000n), - Error, - "Create operation for this UTXO already exists" +Deno.test( + "MoonlightTransactionBuilder - Integration and Edge Cases", + async (t) => { + await t.step( + "should build complete transaction with all operation types", + async () => { + const builder = createTestBuilder(); + const providerKeypair = Keypair.random(); + const userKeypair1 = Keypair.random(); + const userKeypair2 = Keypair.random(); + const mockUtxo1 = { + publicKey: mockUTXO1, + privateKey: Buffer.alloc(32, 0x01), + signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42), + }; + const mockUtxo2 = { + publicKey: mockUTXO2, + privateKey: Buffer.alloc(32, 0x02), + signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x43), + }; + const expirationLedger = 1000; + + // Add all types of operations + builder + .addCreate(mockUTXO1, 1000n) + .addCreate(mockUTXO2, 2000n) + .addSpend(mockUTXO1, [mockCreateCondition]) + .addSpend(mockUTXO2, [mockDepositCondition, mockWithdrawCondition]) + .addDeposit(userKeypair1.publicKey() as `G${string}`, 500n, [ + mockDepositCondition, + ]) + .addDeposit(userKeypair2.publicKey() as `G${string}`, 300n, [ + mockWithdrawCondition, + ]) + .addWithdraw(userKeypair1.publicKey() as `G${string}`, 200n, [ + mockWithdrawCondition, + ]) + .addWithdraw(userKeypair2.publicKey() as `G${string}`, 100n, [ + mockCreateCondition, + ]); + + // Sign with all methods + await builder.signWithProvider(providerKeypair, expirationLedger); + await builder.signWithSpendUtxo(mockUtxo1, expirationLedger); + await builder.signWithSpendUtxo(mockUtxo2, expirationLedger); + await builder.signExtWithEd25519(userKeypair1, expirationLedger); + await builder.signExtWithEd25519(userKeypair2, expirationLedger); + + // Verify all components work together + const operations = builder.getOperation(); + const signedEntries = builder.getSignedAuthEntries(); + const xdr = builder.buildXDR(); + const xdrString = builder.signaturesXDR(); + + // Validate operations + assertEquals(operations.create.length, 2); + assertEquals(operations.spend.length, 2); + assertEquals(operations.deposit.length, 2); + assertEquals(operations.withdraw.length, 2); + + // Validate signatures + assertEquals(Array.isArray(signedEntries), true); + assertEquals(signedEntries.length >= 3, true); // Provider + 2 external + + // Validate XDR + assertEquals(!!xdr, true); + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + } ); - // Test with empty public key - should work but be a valid key - const emptyKey = ("G" + "A".repeat(55)) as `G${string}`; // Valid format but empty content - builder.addDeposit(emptyKey, 500n, []); - - // Try to add the same public key again - should throw error - assertThrows( - () => builder.addDeposit(emptyKey, 1000n, []), - Error, - "Deposit operation for this public key already exists" + await t.step( + "should handle complex transaction with multiple signatures", + async () => { + const builder = createTestBuilder(); + const providerKeypair1 = Keypair.random(); + const providerKeypair2 = Keypair.random(); + const userKeypair1 = Keypair.random(); + const userKeypair2 = Keypair.random(); + const userKeypair3 = Keypair.random(); + const expirationLedger = 1000; + + // Add operations - each user keypair needs both deposit and withdraw operations + builder + .addCreate(mockUTXO1, 1000n) + .addSpend(mockUTXO1, [mockCreateCondition]) + .addDeposit(userKeypair1.publicKey() as `G${string}`, 500n, [ + mockDepositCondition, + ]) + .addDeposit(userKeypair2.publicKey() as `G${string}`, 300n, [ + mockWithdrawCondition, + ]) + .addDeposit(userKeypair3.publicKey() as `G${string}`, 200n, [ + mockWithdrawCondition, + ]) + .addWithdraw(userKeypair1.publicKey() as `G${string}`, 200n, [ + mockWithdrawCondition, + ]) + .addWithdraw(userKeypair2.publicKey() as `G${string}`, 100n, [ + mockCreateCondition, + ]) + .addWithdraw(userKeypair3.publicKey() as `G${string}`, 150n, [ + mockDepositCondition, + ]); + + // Add multiple provider signatures + await builder.signWithProvider(providerKeypair1, expirationLedger); + await builder.signWithProvider(providerKeypair2, expirationLedger); + + // Add multiple external signatures (each keypair has both deposit and withdraw) + await builder.signExtWithEd25519(userKeypair1, expirationLedger); + await builder.signExtWithEd25519(userKeypair2, expirationLedger); + await builder.signExtWithEd25519(userKeypair3, expirationLedger); + + // Verify multiple signatures are handled correctly + const signedEntries = builder.getSignedAuthEntries(); + const xdrString = builder.signaturesXDR(); + + assertEquals(Array.isArray(signedEntries), true); + assertEquals(signedEntries.length >= 4, true); // 2 providers + 3 external + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + } ); - }); - await t.step("should handle concurrent operations", async () => { - const builder = createTestBuilder(); - const providerKeypair = Keypair.random(); - const userKeypair = Keypair.random(); - const mockUtxo = { - publicKey: mockUTXO1, - privateKey: Buffer.alloc(32, 0x01), - signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42) - }; - const expirationLedger = 1000; - - // Add operations - builder - .addCreate(mockUTXO1, 1000n) - .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [mockDepositCondition]); - - // Sign concurrently (simulate concurrent access) - const signingPromises = [ - builder.signWithProvider(providerKeypair, expirationLedger), - builder.signWithSpendUtxo(mockUtxo, expirationLedger), - builder.signExtWithEd25519(userKeypair, expirationLedger) - ]; - - await Promise.all(signingPromises); - - // Verify all signatures were added - const signedEntries = builder.getSignedAuthEntries(); - const xdrString = builder.signaturesXDR(); - - assertEquals(Array.isArray(signedEntries), true); - assertEquals(signedEntries.length >= 2, true); - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - }); - - await t.step("should handle large transaction data", () => { - const builder = createTestBuilder(); - const largeAmount = 999999999999999999n; // Very large amount - const largeUtxo = new Uint8Array(64).fill(0xFF); // Large UTXO - const keypair = Keypair.random(); - - // Add operations with large data - builder - .addCreate(largeUtxo, largeAmount) - .addSpend(largeUtxo, [mockCreateCondition, mockDepositCondition, mockWithdrawCondition]) - .addDeposit(keypair.publicKey() as `G${string}`, largeAmount, [mockDepositCondition]) - .addWithdraw(keypair.publicKey() as `G${string}`, largeAmount, [mockWithdrawCondition]); + await t.step("should validate transaction integrity", async () => { + const builder = createTestBuilder(); + const providerKeypair = Keypair.random(); + const userKeypair = Keypair.random(); + const mockUtxo = { + publicKey: mockUTXO1, + privateKey: Buffer.alloc(32, 0x01), + signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42), + }; + const expirationLedger = 1000; + + // Build transaction + builder + .addCreate(mockUTXO1, 1000n) + .addSpend(mockUTXO1, [mockCreateCondition]) + .addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [ + mockDepositCondition, + ]); - const operations = builder.getOperation(); - const xdr = builder.buildXDR(); + await builder.signWithProvider(providerKeypair, expirationLedger); + await builder.signWithSpendUtxo(mockUtxo, expirationLedger); + await builder.signExtWithEd25519(userKeypair, expirationLedger); + + // Verify transaction integrity + const operations = builder.getOperation(); + const signedEntries = builder.getSignedAuthEntries(); + const xdr = builder.buildXDR(); + const xdrString = builder.signaturesXDR(); + + // All components should be consistent + assertEquals(operations.create.length, 1); + assertEquals(operations.spend.length, 1); + assertEquals(operations.deposit.length, 1); + assertEquals(operations.withdraw.length, 0); + + assertEquals(Array.isArray(signedEntries), true); + assertEquals(signedEntries.length >= 2, true); + assertEquals(!!xdr, true); + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + }); + + await t.step("should handle maximum number of operations", () => { + const builder = createTestBuilder(); + const maxOperations = 10; + + // Add maximum number of each operation type + for (let i = 0; i < maxOperations; i++) { + const utxo = new Uint8Array([ + i, + i + 1, + i + 2, + i + 3, + i + 4, + i + 5, + i + 6, + i + 7, + ]); + const keypair = Keypair.random(); + + builder + .addCreate(utxo, BigInt(1000 + i)) + .addSpend(utxo, [mockCreateCondition]) + .addDeposit(keypair.publicKey() as `G${string}`, BigInt(500 + i), [ + mockDepositCondition, + ]) + .addWithdraw(keypair.publicKey() as `G${string}`, BigInt(300 + i), [ + mockWithdrawCondition, + ]); + } + + const operations = builder.getOperation(); + const xdr = builder.buildXDR(); + + assertEquals(operations.create.length, maxOperations); + assertEquals(operations.spend.length, maxOperations); + assertEquals(operations.deposit.length, maxOperations); + assertEquals(operations.withdraw.length, maxOperations); + assertEquals(!!xdr, true); + }); + + await t.step("should handle edge cases with zero amounts", () => { + const builder = createTestBuilder(); + + // These should throw errors for zero amounts + assertThrows( + () => builder.addCreate(mockUTXO1, 0n), + Error, + "Create operation amount must be positive" + ); + + assertThrows( + () => builder.addDeposit(mockEd25519Key1, 0n, []), + Error, + "Deposit operation amount must be positive" + ); + + assertThrows( + () => builder.addWithdraw(mockEd25519Key1, 0n, []), + Error, + "Withdraw operation amount must be positive" + ); + }); + + await t.step("should handle edge cases with negative amounts", () => { + const builder = createTestBuilder(); + + // These should throw errors for negative amounts + assertThrows( + () => builder.addCreate(mockUTXO1, -100n), + Error, + "Create operation amount must be positive" + ); + + assertThrows( + () => builder.addDeposit(mockEd25519Key1, -100n, []), + Error, + "Deposit operation amount must be positive" + ); + + assertThrows( + () => builder.addWithdraw(mockEd25519Key1, -100n, []), + Error, + "Withdraw operation amount must be positive" + ); + }); + + await t.step("should handle invalid input parameters", () => { + const builder = createTestBuilder(); + + // Test with empty UTXO array - should work but be a valid UTXO + const emptyUtxo = new Uint8Array(8).fill(0); + builder.addCreate(emptyUtxo, 1000n); + + // Try to add the same UTXO again - should throw error + assertThrows( + () => builder.addCreate(emptyUtxo, 2000n), + Error, + "Create operation for this UTXO already exists" + ); + + // Test with empty public key - should work but be a valid key + const emptyKey = ("G" + "A".repeat(55)) as `G${string}`; // Valid format but empty content + builder.addDeposit(emptyKey, 500n, []); + + // Try to add the same public key again - should throw error + assertThrows( + () => builder.addDeposit(emptyKey, 1000n, []), + Error, + "Deposit operation for this public key already exists" + ); + }); + + await t.step("should handle concurrent operations", async () => { + const builder = createTestBuilder(); + const providerKeypair = Keypair.random(); + const userKeypair = Keypair.random(); + const mockUtxo = { + publicKey: mockUTXO1, + privateKey: Buffer.alloc(32, 0x01), + signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42), + }; + const expirationLedger = 1000; + + // Add operations + builder + .addCreate(mockUTXO1, 1000n) + .addSpend(mockUTXO1, [mockCreateCondition]) + .addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [ + mockDepositCondition, + ]); + + // Sign concurrently (simulate concurrent access) + const signingPromises = [ + builder.signWithProvider(providerKeypair, expirationLedger), + builder.signWithSpendUtxo(mockUtxo, expirationLedger), + builder.signExtWithEd25519(userKeypair, expirationLedger), + ]; + + await Promise.all(signingPromises); + + // Verify all signatures were added + const signedEntries = builder.getSignedAuthEntries(); + const xdrString = builder.signaturesXDR(); + + assertEquals(Array.isArray(signedEntries), true); + assertEquals(signedEntries.length >= 2, true); + assertEquals(typeof xdrString, "string"); + assertEquals(xdrString.length > 0, true); + }); + + await t.step("should handle large transaction data", () => { + const builder = createTestBuilder(); + const largeAmount = 999999999999999999n; // Very large amount + const largeUtxo = new Uint8Array(64).fill(0xff); // Large UTXO + const keypair = Keypair.random(); - assertEquals(operations.create.length, 1); - assertEquals(operations.create[0].amount, largeAmount); - assertEquals(operations.spend.length, 1); - assertEquals(operations.deposit.length, 1); - assertEquals(operations.deposit[0].amount, largeAmount); - assertEquals(operations.withdraw.length, 1); - assertEquals(operations.withdraw[0].amount, largeAmount); - assertEquals(!!xdr, true); - }); -}); + // Add operations with large data + builder + .addCreate(largeUtxo, largeAmount) + .addSpend(largeUtxo, [ + mockCreateCondition, + mockDepositCondition, + mockWithdrawCondition, + ]) + .addDeposit(keypair.publicKey() as `G${string}`, largeAmount, [ + mockDepositCondition, + ]) + .addWithdraw(keypair.publicKey() as `G${string}`, largeAmount, [ + mockWithdrawCondition, + ]); + + const operations = builder.getOperation(); + const xdr = builder.buildXDR(); + + assertEquals(operations.create.length, 1); + assertEquals(operations.create[0].amount, largeAmount); + assertEquals(operations.spend.length, 1); + assertEquals(operations.deposit.length, 1); + assertEquals(operations.deposit[0].amount, largeAmount); + assertEquals(operations.withdraw.length, 1); + assertEquals(operations.withdraw[0].amount, largeAmount); + assertEquals(!!xdr, true); + }); + } +); From 12189be9da1978fc8834a919e27383a163a8b83f Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Fri, 17 Oct 2025 10:47:01 -0300 Subject: [PATCH 41/90] test: add unit tests for Condition class operations and error handling - Implemented tests for creating CREATE, DEPOSIT, and WITHDRAW conditions with valid inputs. - Verified correct operation and amount retrieval for each condition type. - Added tests for type guards to identify condition types accurately. - Included tests for converting conditions to XDR and ScVal formats. - Implemented error handling tests for invalid amounts and public key formats. --- src/conditions/index.unit.test.ts | 241 ++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 src/conditions/index.unit.test.ts diff --git a/src/conditions/index.unit.test.ts b/src/conditions/index.unit.test.ts new file mode 100644 index 0000000..8c9759e --- /dev/null +++ b/src/conditions/index.unit.test.ts @@ -0,0 +1,241 @@ +import { assertEquals, assertExists, assertThrows } from "@std/assert"; +import { beforeAll, describe, it } from "@std/testing/bdd"; +import { LocalSigner } from "@colibri/core"; +import type { Ed25519PublicKey } from "@colibri/core"; +import { Condition } from "./index.ts"; +import { UTXOOperation } from "./types.ts"; +import type { UTXOPublicKey } from "../transaction-builder/types.ts"; +import { generateP256KeyPair } from "../utils/secp256r1/generateP256KeyPair.ts"; + +describe("Condition", () => { + let validPublicKey: Ed25519PublicKey; + let validUtxo: UTXOPublicKey; + let validAmount: bigint; + + beforeAll(async () => { + validPublicKey = + LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + validUtxo = (await generateP256KeyPair()).publicKey as UTXOPublicKey; + validAmount = 1000n; + }); + + describe("Construction", () => { + it("should create a CREATE condition with valid inputs", () => { + const condition = Condition.create(validUtxo, validAmount); + + assertExists(condition); + assertEquals(condition.getOperation(), UTXOOperation.CREATE); + assertEquals(condition.getAmount(), validAmount); + assertEquals(condition.getUtxo(), validUtxo); + assertEquals(condition.isCreate(), true); + assertEquals(condition.isDeposit(), false); + assertEquals(condition.isWithdraw(), false); + }); + + it("should create a DEPOSIT condition with valid inputs", () => { + const condition = Condition.deposit(validPublicKey, validAmount); + + assertExists(condition); + assertEquals(condition.getOperation(), UTXOOperation.DEPOSIT); + assertEquals(condition.getAmount(), validAmount); + assertEquals(condition.getPublicKey(), validPublicKey); + assertEquals(condition.isCreate(), false); + assertEquals(condition.isDeposit(), true); + assertEquals(condition.isWithdraw(), false); + }); + + it("should create a WITHDRAW condition with valid inputs", () => { + const condition = Condition.withdraw(validPublicKey, validAmount); + + assertExists(condition); + assertEquals(condition.getOperation(), UTXOOperation.WITHDRAW); + assertEquals(condition.getAmount(), validAmount); + assertEquals(condition.getPublicKey(), validPublicKey); + assertEquals(condition.isCreate(), false); + assertEquals(condition.isDeposit(), false); + assertEquals(condition.isWithdraw(), true); + }); + }); + + describe("Features", () => { + it("should return correct operation for each condition type", () => { + const createCondition = Condition.create(validUtxo, validAmount); + const depositCondition = Condition.deposit(validPublicKey, validAmount); + const withdrawCondition = Condition.withdraw(validPublicKey, validAmount); + + assertEquals(createCondition.getOperation(), UTXOOperation.CREATE); + assertEquals(depositCondition.getOperation(), UTXOOperation.DEPOSIT); + assertEquals(withdrawCondition.getOperation(), UTXOOperation.WITHDRAW); + }); + + it("should return correct amount for all condition types", () => { + const createCondition = Condition.create(validUtxo, validAmount); + const depositCondition = Condition.deposit(validPublicKey, validAmount); + const withdrawCondition = Condition.withdraw(validPublicKey, validAmount); + + assertEquals(createCondition.getAmount(), validAmount); + assertEquals(depositCondition.getAmount(), validAmount); + assertEquals(withdrawCondition.getAmount(), validAmount); + }); + + it("should return UTXO for CREATE conditions", () => { + const condition = Condition.create(validUtxo, validAmount); + assertEquals(condition.getUtxo(), validUtxo); + }); + + it("should return public key for DEPOSIT conditions", () => { + const condition = Condition.deposit(validPublicKey, validAmount); + assertEquals(condition.getPublicKey(), validPublicKey); + }); + + it("should return public key for WITHDRAW conditions", () => { + const condition = Condition.withdraw(validPublicKey, validAmount); + assertEquals(condition.getPublicKey(), validPublicKey); + }); + + it("should correctly identify condition types with type guards", () => { + const createCondition = Condition.create(validUtxo, validAmount); + const depositCondition = Condition.deposit(validPublicKey, validAmount); + const withdrawCondition = Condition.withdraw(validPublicKey, validAmount); + + assertEquals(createCondition.isCreate(), true); + assertEquals(createCondition.isDeposit(), false); + assertEquals(createCondition.isWithdraw(), false); + + assertEquals(depositCondition.isCreate(), false); + assertEquals(depositCondition.isDeposit(), true); + assertEquals(depositCondition.isWithdraw(), false); + + assertEquals(withdrawCondition.isCreate(), false); + assertEquals(withdrawCondition.isDeposit(), false); + assertEquals(withdrawCondition.isWithdraw(), true); + }); + + it("should convert CREATE condition to XDR", () => { + const condition = Condition.create(validUtxo, validAmount); + const xdr = condition.toXDR(); + + assertExists(xdr); + assertEquals(typeof xdr, "string"); + assertEquals(xdr.length > 0, true); + }); + + it("should convert DEPOSIT condition to XDR", () => { + const condition = Condition.deposit(validPublicKey, validAmount); + const xdr = condition.toXDR(); + + assertExists(xdr); + assertEquals(typeof xdr, "string"); + assertEquals(xdr.length > 0, true); + }); + + it("should convert WITHDRAW condition to XDR", () => { + const condition = Condition.withdraw(validPublicKey, validAmount); + const xdr = condition.toXDR(); + + assertExists(xdr); + assertEquals(typeof xdr, "string"); + assertEquals(xdr.length > 0, true); + }); + + it("should convert CREATE condition to ScVal", () => { + const condition = Condition.create(validUtxo, validAmount); + const scVal = condition.toScVal(); + + assertExists(scVal); + assertEquals(scVal.switch().name, "scvVec"); + }); + + it("should convert DEPOSIT condition to ScVal", () => { + const condition = Condition.deposit(validPublicKey, validAmount); + const scVal = condition.toScVal(); + + assertExists(scVal); + assertEquals(scVal.switch().name, "scvVec"); + }); + + it("should convert WITHDRAW condition to ScVal", () => { + const condition = Condition.withdraw(validPublicKey, validAmount); + const scVal = condition.toScVal(); + + assertExists(scVal); + assertEquals(scVal.switch().name, "scvVec"); + }); + }); + + describe("Errors", () => { + it("should throw Error for zero amount", () => { + assertThrows( + () => Condition.create(validUtxo, 0n), + Error, + "Amount must be greater than zero" + ); + }); + + it("should throw Error for negative amount", () => { + assertThrows( + () => Condition.create(validUtxo, -100n), + Error, + "Amount must be greater than zero" + ); + }); + + it("should throw Error for invalid public key format in DEPOSIT", () => { + const invalidPublicKey = "invalid_key" as Ed25519PublicKey; + + assertThrows( + () => Condition.deposit(invalidPublicKey, validAmount), + Error, + "Invalid Ed25519 public key" + ); + }); + + it("should throw Error for invalid public key format in WITHDRAW", () => { + const invalidPublicKey = "invalid_key" as Ed25519PublicKey; + + assertThrows( + () => Condition.withdraw(invalidPublicKey, validAmount), + Error, + "Invalid Ed25519 public key" + ); + }); + + it("should throw Error when accessing public key on CREATE condition", () => { + const condition = Condition.create( + validUtxo, + validAmount + ) as unknown as Condition; + assertThrows( + () => condition.getPublicKey(), + Error, + "Property _publicKey is not set in the Condition instance" + ); + }); + + it("should throw Error when accessing UTXO on DEPOSIT condition", () => { + const condition = Condition.deposit( + validPublicKey, + validAmount + ) as unknown as Condition; + + assertThrows( + () => condition.getUtxo(), + Error, + "Property _utxo is not set in the Condition instance" + ); + }); + + it("should throw Error when accessing UTXO on WITHDRAW condition", () => { + const condition = Condition.withdraw( + validPublicKey, + validAmount + ) as unknown as Condition; + + assertThrows( + () => condition.getUtxo(), + Error, + "Property _utxo is not set in the Condition instance" + ); + }); + }); +}); From 301b26841dc14632dca1f74b6f1c6766e73ef6c5 Mon Sep 17 00:00:00 2001 From: Victor Hugo Martins Date: Fri, 17 Oct 2025 16:56:49 -0300 Subject: [PATCH 42/90] Update src/conditions/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/conditions/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conditions/index.ts b/src/conditions/index.ts index 16cdf17..7a129a4 100644 --- a/src/conditions/index.ts +++ b/src/conditions/index.ts @@ -59,7 +59,7 @@ export class Condition implements BaseCondition { utxo?: UTXOPublicKey; }) { if (amount <= 0n) { - throw new Error("Amount must be greater than zero."); + throw new Error("Amount must be greater than zero"); } this._op = op; From 8c1ffa79e4847b4f56cdc39674f92196f568cd5b Mon Sep 17 00:00:00 2001 From: Victor Hugo Martins Date: Fri, 17 Oct 2025 16:57:01 -0300 Subject: [PATCH 43/90] Update src/conditions/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/conditions/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conditions/index.ts b/src/conditions/index.ts index 7a129a4..b741402 100644 --- a/src/conditions/index.ts +++ b/src/conditions/index.ts @@ -116,7 +116,7 @@ export class Condition implements BaseCondition { amount: bigint ): DepositCondition { if (!StrKey.isValidEd25519PublicKey(publicKey)) { - throw new Error("Invalid Ed25519 public key."); + throw new Error("Invalid Ed25519 public key"); } return new Condition({ From cb69b5ec9de501910e87a12b208637e3b8c1b49d Mon Sep 17 00:00:00 2001 From: Victor Hugo Martins Date: Fri, 17 Oct 2025 16:57:14 -0300 Subject: [PATCH 44/90] Update src/conditions/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/conditions/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conditions/index.ts b/src/conditions/index.ts index b741402..69e8795 100644 --- a/src/conditions/index.ts +++ b/src/conditions/index.ts @@ -180,7 +180,7 @@ export class Condition implements BaseCondition { arg: "_op" | "_amount" | "_publicKey" | "_utxo" ): UTXOOperation | bigint | Ed25519PublicKey | UTXOPublicKey { if (this[arg]) return this[arg]; - throw new Error(`Property ${arg} is not set in the Condition instance.`); + throw new Error(`Property ${arg} is not set in the Condition instance`); } //========================================== From 1a2a9c24aef89f289d2350990788e6ad3c1fc28b Mon Sep 17 00:00:00 2001 From: Victor Hugo Martins Date: Fri, 17 Oct 2025 16:57:39 -0300 Subject: [PATCH 45/90] Update src/conditions/types.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/conditions/types.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/conditions/types.ts b/src/conditions/types.ts index eea5b85..bd2317c 100644 --- a/src/conditions/types.ts +++ b/src/conditions/types.ts @@ -1,27 +1,7 @@ -// export type CreateCondition = { -// action: "CREATE"; -// utxo: Uint8Array; -// amount: bigint; -// }; import type { Ed25519PublicKey } from "@colibri/core"; import type { UTXOPublicKey } from "../transaction-builder/types.ts"; import type { xdr } from "@stellar/stellar-sdk"; - -// export type DepositCondition = { -// action: "DEPOSIT"; -// publicKey: string; -// amount: bigint; -// }; - -// export type WithdrawCondition = { -// action: "WITHDRAW"; -// publicKey: string; -// amount: bigint; -// }; - -// export type Condition = CreateCondition | DepositCondition | WithdrawCondition; - export enum UTXOOperation { CREATE = "Create", DEPOSIT = "Deposit", From 7660d65e5c2d67f2d8e9b20714eebc68dab1e23e Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Mon, 20 Oct 2025 11:00:13 -0300 Subject: [PATCH 46/90] feat: enhance PrivacyChannel and MoonlightTransactionBuilder with new invoke methods and transaction signing - Added `invokeRaw` method to `PrivacyChannel` for direct contract invocation with operation arguments. - Introduced `getInvokeOperation` method in `MoonlightTransactionBuilder` to create invoke operations. - Updated `signWithProvider` and `signExtWithEd25519` methods to support `TransactionSigner` type. - Enhanced integration tests for `PrivacyChannel` to include contract invocation and UTXO balance checks. - Improved error handling in contract invocation to log transaction details on failure. --- src/privacy-channel/index.ts | 24 ++++ src/transaction-builder/index.ts | 50 +++++-- .../privacy-channel.integration.test.ts | 132 ++++++++++++++++-- 3 files changed, 182 insertions(+), 24 deletions(-) diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index 1b53592..1f77f96 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -12,6 +12,7 @@ import { ChannelSpec, } from "./constants.ts"; import type { ChannelInvoke, ChannelRead } from "./types.ts"; +import { xdr } from "@stellar/stellar-sdk"; export class PrivacyChannel { private _client: Contract; @@ -175,8 +176,31 @@ export class PrivacyChannel { public async invoke(args: { method: M; methodArgs: ChannelInvoke[M]["input"]; + auth?: xdr.SorobanAuthorizationEntry[]; config: TransactionConfig; }) { return await this.getClient().invoke(args); } + + /** + * Invoke the contract function directly using the specified operation arguments. + * + * @param args + * @param { operationArgs } args.operationArgs - The operation arguments for the invoke. + * @param { string } args.operationArgs.function - The function name to invoke. + * @param { xdr.ScVal[] } args.operationArgs.args - The arguments for the function. + * @param { xdr.SorobanAuthorizationEntry[] } [args.operationArgs.auth] - Optional authorization entries. + * @param { TransactionConfig } args.config - The transaction configuration. + * @returns {ReturnType} A promise that resolves to the invoke colibri response. + * */ + public async invokeRaw(args: { + operationArgs: { + function: string; + args: xdr.ScVal[]; + auth?: xdr.SorobanAuthorizationEntry[]; + }; + config: TransactionConfig; + }) { + return await this.getClient().invokeRaw(args); + } } diff --git a/src/transaction-builder/index.ts b/src/transaction-builder/index.ts index ee0b94d..e341388 100644 --- a/src/transaction-builder/index.ts +++ b/src/transaction-builder/index.ts @@ -2,6 +2,7 @@ import { type Asset, authorizeEntry, type Keypair, + Operation, xdr, } from "@stellar/stellar-sdk"; import { Buffer } from "buffer"; @@ -40,6 +41,7 @@ import { assertSpendExists, } from "./validators/index.ts"; import type { Condition } from "../conditions/types.ts"; +import { type TransactionSigner, isTransactionSigner } from "@colibri/core"; export class MoonlightTransactionBuilder { private create: CreateOperation[] = []; @@ -283,19 +285,25 @@ export class MoonlightTransactionBuilder { } async signWithProvider( - providerKeys: Keypair, + providerKeys: TransactionSigner | Keypair, signatureExpirationLedger: number, nonce?: string ) { if (!nonce) nonce = generateNonce(); - const signedHash = providerKeys.sign( - await this.getOperationAuthEntryHash(nonce, signatureExpirationLedger) + const authHash = await this.getOperationAuthEntryHash( + nonce, + signatureExpirationLedger ); + const signedHash = isTransactionSigner(providerKeys) + ? // deno-lint-ignore no-explicit-any + providerKeys.sign(authHash as any) + : providerKeys.sign(authHash); + this.addProviderInnerSignature( providerKeys.publicKey() as Ed25519PublicKey, - signedHash, + signedHash as Buffer, signatureExpirationLedger, nonce ); @@ -326,7 +334,7 @@ export class MoonlightTransactionBuilder { } async signExtWithEd25519( - keys: Keypair, + keys: TransactionSigner | Keypair, signatureExpirationLedger: number, nonce?: string ) { @@ -338,12 +346,21 @@ export class MoonlightTransactionBuilder { signatureExpirationLedger ); - const signedAuthEntry = await authorizeEntry( - rawAuthEntry, - keys, - signatureExpirationLedger, - this.network - ); + let signedAuthEntry: xdr.SorobanAuthorizationEntry; + if (isTransactionSigner(keys)) { + signedAuthEntry = await keys.signSorobanAuthEntry( + rawAuthEntry, + signatureExpirationLedger, + this.network + ); + } else { + signedAuthEntry = await authorizeEntry( + rawAuthEntry, + keys, + signatureExpirationLedger, + this.network + ); + } this.addExtSignedEntry( keys.publicKey() as Ed25519PublicKey, @@ -359,6 +376,17 @@ export class MoonlightTransactionBuilder { return signedEntries; } + getInvokeOperation(): xdr.Operation { + return Operation.invokeContractFunction({ + contract: this.channelId, + + function: "transact", + + args: [this.buildXDR()], + auth: [...this.getSignedAuthEntries()], + }); + } + buildXDR(): xdr.ScVal { return xdr.ScVal.scvMap([ new xdr.ScMapEntry({ diff --git a/test/integration/privacy-channel.integration.test.ts b/test/integration/privacy-channel.integration.test.ts index 0bb057a..9ed1a37 100644 --- a/test/integration/privacy-channel.integration.test.ts +++ b/test/integration/privacy-channel.integration.test.ts @@ -7,41 +7,60 @@ import { TestNet, initializeWithFriendbot, Contract, + P_SimulateTransactionErrors, } from "@colibri/core"; import type { Ed25519PublicKey, TransactionConfig, ContractId, + TestNetConfig, + Ed25519SecretKey, } from "@colibri/core"; -import { AuthSpec } from "../../src/channel-auth/constants.ts"; +import { + AuthInvokeMethods, + AuthSpec, +} from "../../src/channel-auth/constants.ts"; import type { Buffer } from "node:buffer"; import { loadContractWasm } from "../helpers/load-wasm.ts"; import type { ChannelAuthConstructorArgs } from "../../src/channel-auth/types.ts"; import type { ChannelConstructorArgs } from "../../src/privacy-channel/types.ts"; import { + ChannelInvokeMethods, ChannelReadMethods, ChannelSpec, } from "../../src/privacy-channel/constants.ts"; -import { Asset } from "@stellar/stellar-sdk"; +import { Asset, Keypair } from "@stellar/stellar-sdk"; import { PrivacyChannel } from "../../src/privacy-channel/index.ts"; import { disableSanitizeConfig } from "../utils/disable-sanitize-config.ts"; import { generateP256KeyPair } from "../../src/utils/secp256r1/generateP256KeyPair.ts"; +import { MoonlightTransactionBuilder } from "../../src/transaction-builder/index.ts"; +import { Condition } from "../../src/conditions/index.ts"; +import { Server } from "@stellar/stellar-sdk/rpc"; +import { generateNonce } from "../../src/utils/common/index.ts"; describe( "[Testnet - Integration] PrivacyChannel", disableSanitizeConfig, () => { - const networkConfig = TestNet(); + const networkConfig: TestNetConfig = TestNet(); const admin = NativeAccount.fromMasterSigner(LocalSigner.generateRandom()); + + const providerKeys = Keypair.random(); + const johnKeys = Keypair.random(); + const providerA = NativeAccount.fromMasterSigner( - LocalSigner.generateRandom() + LocalSigner.fromSecret(providerKeys.secret() as Ed25519SecretKey) + ); + + const john = NativeAccount.fromMasterSigner( + LocalSigner.fromSecret(johnKeys.secret() as Ed25519SecretKey) ); const txConfig: TransactionConfig = { fee: "1000000", - timeout: 30, + timeout: 60, source: admin.address(), signers: [admin.signer()], }; @@ -50,6 +69,8 @@ describe( networkConfig.networkPassphrase ) as ContractId; + let rpc: Server; + let authWasm: Buffer; let channelWasm: Buffer; let authId: ContractId; @@ -66,6 +87,13 @@ describe( providerA.address() as Ed25519PublicKey ); + await initializeWithFriendbot( + networkConfig.friendbotUrl, + john.address() as Ed25519PublicKey + ); + + rpc = new Server(networkConfig.rpcUrl as string); + authWasm = loadContractWasm("channel_auth_contract"); channelWasm = loadContractWasm("privacy_channel"); @@ -89,6 +117,14 @@ describe( }); authId = authContract.getContractId(); + + await authContract.invoke({ + method: AuthInvokeMethods.add_provider, + methodArgs: { + provider: providerA.address() as Ed25519PublicKey, + }, + config: { ...txConfig, signers: [admin.signer(), providerA.signer()] }, + }); }); describe("Basic tests", () => { @@ -184,14 +220,84 @@ describe( assertEquals(utxoBal, -1n); // UTXO is in Unused state }); - //TODO: Complete this test once we have the tx builder - it.skip("should invoke the contract", async () => { - // const channelClient = new PrivacyChannel( - // networkConfig, - // channelId, - // authId, - // assetId - // ); + it("should invoke the contract", async () => { + const channelClient = new PrivacyChannel( + networkConfig, + channelId, + authId, + assetId + ); + + const utxoAKeypair = await generateP256KeyPair(); + const utxoBKeypair = await generateP256KeyPair(); + + const depositTx = new MoonlightTransactionBuilder({ + network: networkConfig.networkPassphrase, + channelId: channelId, + authId: authId, + asset: Asset.native(), + }); + + depositTx.addDeposit(john.address() as Ed25519PublicKey, 500n, [ + Condition.create(utxoAKeypair.publicKey, 250n), + Condition.create(utxoBKeypair.publicKey, 250n), + ]); + + depositTx.addCreate(utxoAKeypair.publicKey, 250n); + depositTx.addCreate(utxoBKeypair.publicKey, 250n); + + const latestLedger = await rpc.getLatestLedger(); + + const signatureExpirationLedger = latestLedger.sequence + 100; + + const nonce = generateNonce(); + + await depositTx.signExtWithEd25519( + johnKeys, + signatureExpirationLedger, + nonce + ); + + await depositTx.signWithProvider( + providerKeys, + signatureExpirationLedger, + nonce + ); + + await channelClient + .invokeRaw({ + operationArgs: { + function: ChannelInvokeMethods.transact, + args: [depositTx.buildXDR()], + auth: [...depositTx.getSignedAuthEntries()], + }, + config: txConfig, + }) + .catch((e) => { + if (e instanceof P_SimulateTransactionErrors.SIMULATION_FAILED) { + console.error("Error invoking contract:", e); + console.error( + "Transaction XDR:", + e.meta.data.input.transaction.toXDR() + ); + } + throw e; + }); + + const utxoABal = await channelClient.read({ + method: ChannelReadMethods.utxo_balance, + methodArgs: { utxo: utxoAKeypair.publicKey as Buffer }, + }); + + const utxoBBal = await channelClient.read({ + method: ChannelReadMethods.utxo_balance, + methodArgs: { utxo: utxoBKeypair.publicKey as Buffer }, + }); + + assertExists(utxoABal); + assertEquals(utxoABal, 250n); + assertExists(utxoBBal); + assertEquals(utxoBBal, 250n); }); }); } From 77f2acfa82cb90792f29052a0b77b4375ba8df90 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Mon, 20 Oct 2025 11:00:24 -0300 Subject: [PATCH 47/90] chore: update dependencies for @colibri/core and @stellar/stellar-sdk to latest versions for improved compatibility and performance --- deno.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/deno.json b/deno.json index 712f371..a0c09c0 100644 --- a/deno.json +++ b/deno.json @@ -14,8 +14,7 @@ "exports": "./mod.ts", "imports": { "@fifo/convee": "jsr:@fifo/convee@^0.5.0", - "@colibri/core": "jsr:@colibri/core@^0.4.1", - "@stellar/stellar-sdk": "npm:@stellar/stellar-sdk@^14.0.0", + "@colibri/core": "jsr:@colibri/core@^0.5.3", "@noble/curves": "jsr:@noble/curves", "@noble/hashes": "jsr:@noble/hashes", From 0e395a03a62927a1baa15724853125e1706e8c21 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Tue, 21 Oct 2025 09:02:08 -0300 Subject: [PATCH 48/90] refactor: update UTXO operation handling in Condition class and related tests - Changed UTXOOperation to UTXOOperationType in Condition class to improve type safety and consistency. - Updated method signatures and return types in BaseCondition, CreateCondition, DepositCondition, and WithdrawCondition to reflect the new UTXOOperationType. - Modified unit tests to align with the updated operation types for create, deposit, and withdraw conditions. --- src/conditions/index.ts | 39 ++++++++++++++++--------------- src/conditions/index.unit.test.ts | 19 ++++++++------- src/conditions/types.ts | 18 ++++++-------- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/conditions/index.ts b/src/conditions/index.ts index 69e8795..2e160e7 100644 --- a/src/conditions/index.ts +++ b/src/conditions/index.ts @@ -1,14 +1,15 @@ import { StrKey, type Ed25519PublicKey } from "@colibri/core"; import { nativeToScVal, xdr } from "@stellar/stellar-sdk"; -import type { UTXOPublicKey } from "../transaction-builder/types.ts"; + import { Buffer } from "node:buffer"; -import { - UTXOOperation, - type BaseCondition, - type CreateCondition, - type DepositCondition, - type WithdrawCondition, +import type { + BaseCondition, + CreateCondition, + DepositCondition, + WithdrawCondition, } from "./types.ts"; +import { UTXOOperationType } from "../operation/types.ts"; +import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; /** * Represents a condition for UTXO operations in the Moonlight privacy protocol. @@ -31,7 +32,7 @@ import { * ``` */ export class Condition implements BaseCondition { - private _op: UTXOOperation; + private _op: UTXOOperationType; private _amount: bigint; private _publicKey?: Ed25519PublicKey; private _utxo?: UTXOPublicKey; @@ -53,7 +54,7 @@ export class Condition implements BaseCondition { publicKey, utxo, }: { - op: UTXOOperation; + op: UTXOOperationType; amount: bigint; publicKey?: Ed25519PublicKey; utxo?: UTXOPublicKey; @@ -87,7 +88,7 @@ export class Condition implements BaseCondition { */ static create(utxo: UTXOPublicKey, amount: bigint): CreateCondition { return new Condition({ - op: UTXOOperation.CREATE, + op: UTXOOperationType.CREATE, utxo, amount, }) as CreateCondition; @@ -120,7 +121,7 @@ export class Condition implements BaseCondition { } return new Condition({ - op: UTXOOperation.DEPOSIT, + op: UTXOOperationType.DEPOSIT, publicKey, amount, }) as DepositCondition; @@ -153,7 +154,7 @@ export class Condition implements BaseCondition { throw new Error("Invalid Ed25519 public key."); } return new Condition({ - op: UTXOOperation.WITHDRAW, + op: UTXOOperationType.WITHDRAW, publicKey, amount, }) as WithdrawCondition; @@ -172,13 +173,13 @@ export class Condition implements BaseCondition { * @throws {Error} If the requested property is not set * @private */ - private require(arg: "_op"): UTXOOperation; + private require(arg: "_op"): UTXOOperationType; private require(arg: "_amount"): bigint; private require(arg: "_publicKey"): Ed25519PublicKey; private require(arg: "_utxo"): UTXOPublicKey; private require( arg: "_op" | "_amount" | "_publicKey" | "_utxo" - ): UTXOOperation | bigint | Ed25519PublicKey | UTXOPublicKey { + ): UTXOOperationType | bigint | Ed25519PublicKey | UTXOPublicKey { if (this[arg]) return this[arg]; throw new Error(`Property ${arg} is not set in the Condition instance`); } @@ -199,7 +200,7 @@ export class Condition implements BaseCondition { * console.log(condition.getOperation()); // "Create" * ``` */ - public getOperation(): UTXOOperation { + public getOperation(): UTXOOperationType { return this.require("_op"); } @@ -278,7 +279,7 @@ export class Condition implements BaseCondition { public toScVal(): xdr.ScVal { const actionScVal = xdr.ScVal.scvSymbol(this.getOperation()); const addressScVal = - this.getOperation() === UTXOOperation.CREATE + this.getOperation() === UTXOOperationType.CREATE ? xdr.ScVal.scvBytes(Buffer.from(this.getUtxo())) : nativeToScVal(this.getPublicKey(), { type: "address" }); const amountScVal = nativeToScVal(this.getAmount(), { type: "i128" }); @@ -330,7 +331,7 @@ export class Condition implements BaseCondition { * ``` */ public isCreate(): this is CreateCondition { - return this.getOperation() === UTXOOperation.CREATE; + return this.getOperation() === UTXOOperationType.CREATE; } /** @@ -349,7 +350,7 @@ export class Condition implements BaseCondition { * ``` */ public isDeposit(): this is DepositCondition { - return this.getOperation() === UTXOOperation.DEPOSIT; + return this.getOperation() === UTXOOperationType.DEPOSIT; } /** @@ -368,6 +369,6 @@ export class Condition implements BaseCondition { * ``` */ public isWithdraw(): this is WithdrawCondition { - return this.getOperation() === UTXOOperation.WITHDRAW; + return this.getOperation() === UTXOOperationType.WITHDRAW; } } diff --git a/src/conditions/index.unit.test.ts b/src/conditions/index.unit.test.ts index 8c9759e..a073f20 100644 --- a/src/conditions/index.unit.test.ts +++ b/src/conditions/index.unit.test.ts @@ -3,9 +3,9 @@ import { beforeAll, describe, it } from "@std/testing/bdd"; import { LocalSigner } from "@colibri/core"; import type { Ed25519PublicKey } from "@colibri/core"; import { Condition } from "./index.ts"; -import { UTXOOperation } from "./types.ts"; -import type { UTXOPublicKey } from "../transaction-builder/types.ts"; import { generateP256KeyPair } from "../utils/secp256r1/generateP256KeyPair.ts"; +import { UTXOOperationType } from "../operation/types.ts"; +import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; describe("Condition", () => { let validPublicKey: Ed25519PublicKey; @@ -24,7 +24,7 @@ describe("Condition", () => { const condition = Condition.create(validUtxo, validAmount); assertExists(condition); - assertEquals(condition.getOperation(), UTXOOperation.CREATE); + assertEquals(condition.getOperation(), UTXOOperationType.CREATE); assertEquals(condition.getAmount(), validAmount); assertEquals(condition.getUtxo(), validUtxo); assertEquals(condition.isCreate(), true); @@ -36,7 +36,7 @@ describe("Condition", () => { const condition = Condition.deposit(validPublicKey, validAmount); assertExists(condition); - assertEquals(condition.getOperation(), UTXOOperation.DEPOSIT); + assertEquals(condition.getOperation(), UTXOOperationType.DEPOSIT); assertEquals(condition.getAmount(), validAmount); assertEquals(condition.getPublicKey(), validPublicKey); assertEquals(condition.isCreate(), false); @@ -48,7 +48,7 @@ describe("Condition", () => { const condition = Condition.withdraw(validPublicKey, validAmount); assertExists(condition); - assertEquals(condition.getOperation(), UTXOOperation.WITHDRAW); + assertEquals(condition.getOperation(), UTXOOperationType.WITHDRAW); assertEquals(condition.getAmount(), validAmount); assertEquals(condition.getPublicKey(), validPublicKey); assertEquals(condition.isCreate(), false); @@ -63,9 +63,12 @@ describe("Condition", () => { const depositCondition = Condition.deposit(validPublicKey, validAmount); const withdrawCondition = Condition.withdraw(validPublicKey, validAmount); - assertEquals(createCondition.getOperation(), UTXOOperation.CREATE); - assertEquals(depositCondition.getOperation(), UTXOOperation.DEPOSIT); - assertEquals(withdrawCondition.getOperation(), UTXOOperation.WITHDRAW); + assertEquals(createCondition.getOperation(), UTXOOperationType.CREATE); + assertEquals(depositCondition.getOperation(), UTXOOperationType.DEPOSIT); + assertEquals( + withdrawCondition.getOperation(), + UTXOOperationType.WITHDRAW + ); }); it("should return correct amount for all condition types", () => { diff --git a/src/conditions/types.ts b/src/conditions/types.ts index bd2317c..68c07af 100644 --- a/src/conditions/types.ts +++ b/src/conditions/types.ts @@ -1,15 +1,11 @@ - import type { Ed25519PublicKey } from "@colibri/core"; -import type { UTXOPublicKey } from "../transaction-builder/types.ts"; + import type { xdr } from "@stellar/stellar-sdk"; -export enum UTXOOperation { - CREATE = "Create", - DEPOSIT = "Deposit", - WITHDRAW = "Withdraw", -} +import type { UTXOOperationType } from "../operation/types.ts"; +import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; export type BaseCondition = { - getOperation(): UTXOOperation; + getOperation(): UTXOOperationType; getAmount(): bigint; isCreate(): this is CreateCondition; isDeposit(): this is DepositCondition; @@ -19,17 +15,17 @@ export type BaseCondition = { }; export type CreateCondition = BaseCondition & { - getOperation(): UTXOOperation.CREATE; + getOperation(): UTXOOperationType.CREATE; getUtxo(): UTXOPublicKey; }; export type DepositCondition = BaseCondition & { - getOperation(): UTXOOperation.DEPOSIT; + getOperation(): UTXOOperationType.DEPOSIT; getPublicKey(): Ed25519PublicKey; }; export type WithdrawCondition = BaseCondition & { - getOperation(): UTXOOperation.WITHDRAW; + getOperation(): UTXOOperationType.WITHDRAW; getPublicKey(): Ed25519PublicKey; }; From 59451e6215091b27c2362410f1266e68747f7555 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Tue, 21 Oct 2025 09:02:41 -0300 Subject: [PATCH 49/90] refactor: change import statements to use 'type' for xdr in bundle and deposit auth entry files, remove unused types and utility functions - Updated import statements in `bundle-auth-entry.ts` and `deposit-auth-entry.ts` to use `import type` for better clarity and performance. - Deleted unused `types.ts` and `auth-entries.ts` files to clean up the codebase and remove deprecated types and functions. --- .../auth/bundle-auth-entry.ts | 4 +- .../auth/deposit-auth-entry.ts | 4 +- src/transaction-builder/types.ts | 28 ---- src/utils/auth/auth-entries.ts | 149 ------------------ src/utils/types/stellar.types.ts | 3 - 5 files changed, 2 insertions(+), 186 deletions(-) delete mode 100644 src/transaction-builder/types.ts delete mode 100644 src/utils/auth/auth-entries.ts delete mode 100644 src/utils/types/stellar.types.ts diff --git a/src/transaction-builder/auth/bundle-auth-entry.ts b/src/transaction-builder/auth/bundle-auth-entry.ts index 304662a..804e1c8 100644 --- a/src/transaction-builder/auth/bundle-auth-entry.ts +++ b/src/transaction-builder/auth/bundle-auth-entry.ts @@ -1,4 +1,4 @@ -import { xdr } from "@stellar/stellar-sdk"; +import type { xdr } from "@stellar/stellar-sdk"; import { generateBundleAuthEntry } from "../../utils/auth/bundle-auth-entry.ts"; export const buildBundleAuthEntry = (args: { @@ -11,5 +11,3 @@ export const buildBundleAuthEntry = (args: { }): xdr.SorobanAuthorizationEntry => { return generateBundleAuthEntry(args); }; - - diff --git a/src/transaction-builder/auth/deposit-auth-entry.ts b/src/transaction-builder/auth/deposit-auth-entry.ts index 1c371bd..d32c39e 100644 --- a/src/transaction-builder/auth/deposit-auth-entry.ts +++ b/src/transaction-builder/auth/deposit-auth-entry.ts @@ -1,4 +1,4 @@ -import { xdr } from "@stellar/stellar-sdk"; +import type { xdr } from "@stellar/stellar-sdk"; import { generateDepositAuthEntry } from "../../utils/auth/deposit-auth-entry.ts"; export const buildDepositAuthEntry = (args: { @@ -12,5 +12,3 @@ export const buildDepositAuthEntry = (args: { }): xdr.SorobanAuthorizationEntry => { return generateDepositAuthEntry(args); }; - - diff --git a/src/transaction-builder/types.ts b/src/transaction-builder/types.ts deleted file mode 100644 index 9a93029..0000000 --- a/src/transaction-builder/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Condition } from "../conditions/types.ts"; - -export type MoonlightOperation = { - create: CreateOperation[]; - spend: SpendOperation[]; - deposit: DepositOperation[]; - withdraw: WithdrawOperation[]; -}; - -export type SpendOperation = { utxo: UTXOPublicKey; conditions: Condition[] }; - -export type DepositOperation = { - pubKey: Ed25519PublicKey; - amount: bigint; - conditions: Condition[]; -}; - -export type WithdrawOperation = { - pubKey: Ed25519PublicKey; - amount: bigint; - conditions: Condition[]; -}; - -export type CreateOperation = { utxo: UTXOPublicKey; amount: bigint }; - -export type UTXOPublicKey = Uint8Array; - -export type Ed25519PublicKey = `G${string}`; diff --git a/src/utils/auth/auth-entries.ts b/src/utils/auth/auth-entries.ts deleted file mode 100644 index 30c980d..0000000 --- a/src/utils/auth/auth-entries.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Address, nativeToScVal, scValToNative, xdr } from "@stellar/stellar-sdk"; - -// deno-lint-ignore-file no-explicit-any -export interface AuthEntryParams { - credentials: { - address: string; - nonce: string; - signatureExpirationLedger: number; - signature?: string; - }; - rootInvocation: InvocationParams; -} - -export interface InvocationParams { - function: { - contractAddress: string; - functionName: string; - args: FnArg[] | xdr.ScVal[]; - }; - subInvocations?: InvocationParams[]; -} - -export interface FnArg { - value: any; - type: string; -} - -const invocationToParams = ( - invocation: xdr.SorobanAuthorizedInvocation -): InvocationParams => { - return { - function: { - contractAddress: Address.fromScAddress( - invocation.function().contractFn().contractAddress() - ).toString(), - functionName: invocation - .function() - .contractFn() - .functionName() - .toString(), - args: invocation.function().contractFn().args().map(parseScVal), - }, - subInvocations: [ - ...invocation - .subInvocations() - .map((subInvocation) => invocationToParams(subInvocation)), - ], - }; -}; - -export const paramsToInvocation = ( - params: InvocationParams -): xdr.SorobanAuthorizedInvocation => { - let args; - - if (params.function.args.length > 0 && "type" in params.function.args[0]) { - args = (params.function.args as FnArg[]).map((arg) => { - return nativeToScVal(arg.value, { type: arg.type }); - }); - } else { - args = params.function.args as xdr.ScVal[]; - } - - return new xdr.SorobanAuthorizedInvocation({ - function: - xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn( - new xdr.InvokeContractArgs({ - contractAddress: Address.fromString( - params.function.contractAddress - ).toScAddress(), - functionName: params.function.functionName, - args: args, - }) - ), - subInvocations: params.subInvocations?.map(paramsToInvocation) || [], - }); -}; - -export const authEntryToParams = ( - entry: xdr.SorobanAuthorizationEntry -): AuthEntryParams => { - const credentials = entry.credentials(); - const rootInvocation = entry.rootInvocation(); - - const entryParams: AuthEntryParams = { - credentials: { - address: Address.fromScAddress( - credentials.address().address() - ).toString(), - nonce: credentials.address().nonce().toString(), - signatureExpirationLedger: credentials - .address() - .signatureExpirationLedger(), - signature: credentials.address().signature().toXDR("base64"), - }, - rootInvocation: invocationToParams(rootInvocation), - }; - - return entryParams; -}; - -const parseScVal = (value: xdr.ScVal): FnArg => { - const type = parseScValType(value.switch().name); - return { - value: - type === "bool" ? scValToNative(value) : String(scValToNative(value)), - type, - }; -}; - -const parseScValType = (rawType: string): string => { - switch (rawType) { - case "scvAddress": - return "address"; - case "scvI128": - return "i128"; - case "scvBool": - return "bool"; - - default: - return rawType; - } -}; - -export const paramsToAuthEntry = ( - param: AuthEntryParams -): xdr.SorobanAuthorizationEntry => { - const credParams = param.credentials; - - return new xdr.SorobanAuthorizationEntry({ - rootInvocation: paramsToInvocation(param.rootInvocation), - credentials: xdr.SorobanCredentials.sorobanCredentialsAddress( - new xdr.SorobanAddressCredentials({ - address: Address.fromString(credParams.address).toScAddress(), - nonce: new xdr.Int64(credParams.nonce), - signatureExpirationLedger: credParams.signatureExpirationLedger, - signature: !credParams.signature - ? xdr.ScVal.scvVoid() - : xdr.ScVal.fromXDR(credParams.signature!, "base64"), - }) - ), - }); -}; - -export const paramsToAuthEntries = ( - authEntryParams: AuthEntryParams[] -): xdr.SorobanAuthorizationEntry[] => { - return authEntryParams.map(paramsToAuthEntry); -}; diff --git a/src/utils/types/stellar.types.ts b/src/utils/types/stellar.types.ts deleted file mode 100644 index 01ba1ca..0000000 --- a/src/utils/types/stellar.types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type StellarSecretKey = `S${string}`; -export type StellarSmartContractId = `C${string}`; -// TODO: Consider move this types to use direct from Colibri (SEP-23 strkey) From 50e57672345e9cbca3aabf3c2b8f100dcadde42a Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Tue, 21 Oct 2025 09:03:02 -0300 Subject: [PATCH 50/90] refactor: comment out unused imports and code in full-setup.ts and utxo-based-account.integration.test.ts for cleaner codebase - Commented out all import statements and code in `full-setup.ts` to prevent execution during tests and maintain focus on relevant functionality. - Commented out the entire test suite in `utxo-based-account.integration.test.ts` to temporarily disable tests while refactoring and improving the code structure. - Added a lint ignore comment for `no-explicit-any` in `traverse-object-log.ts` to suppress TypeScript warnings for the `obj` parameter. --- .../utxo-based-account.integration.test.ts | 907 +++++++++--------- test/scripts/full-setup.ts | 196 ++-- test/utils/traverse-object-log.ts | 1 + 3 files changed, 551 insertions(+), 553 deletions(-) diff --git a/test/integration/utxo-based-account.integration.test.ts b/test/integration/utxo-based-account.integration.test.ts index da4d065..5c84598 100644 --- a/test/integration/utxo-based-account.integration.test.ts +++ b/test/integration/utxo-based-account.integration.test.ts @@ -1,455 +1,452 @@ -// deno-lint-ignore-file require-await -import { - assertEquals, - assertExists, -} from "https://deno.land/std@0.207.0/assert/mod.ts"; -import { Buffer } from "buffer"; -import { - type PoolEngine, - ReadMethods, - WriteMethods, - UtxoBasedStellarAccount, - UTXOStatus, -} from "../../mod.ts"; -import { createTestAccount } from "../helpers/create-test-account.ts"; -import { createTxInvocation } from "../helpers/create-tx-invocation.ts"; -import { deployPrivacyPool } from "../helpers/deploy-pool.ts"; -import type { SorobanTransactionPipelineOutputVerbose } from "stellar-plus/lib/stellar-plus/core/pipelines/soroban-transaction/types"; - -// Testnet XLM contract ID -const XLM_CONTRACT_ID_TESTNET = - "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; - -Deno.test("UTXOBasedAccount Integration Tests", async (t) => { - const { account, networkConfig } = await createTestAccount(); - const txInvocation = createTxInvocation(account); - let poolEngine: PoolEngine; - let utxoAccount: UtxoBasedStellarAccount; - const depositAmount = 500000n; // 0.05 XLM - const testRoot = "S-TEST_SECRET_ROOT"; - - // Setup test environment - await t.step("setup: deploy privacy pool contract", async () => { - poolEngine = await deployPrivacyPool({ - admin: account, - assetContractId: XLM_CONTRACT_ID_TESTNET, - networkConfig, - }); - - assertExists(poolEngine, "Pool engine should be initialized"); - assertExists(poolEngine.getContractId(), "Contract ID should be generated"); - }); - - // Initialize UTXOBasedAccount with pool engine's derivator - directly in the test - await t.step("setup: create UTXOBasedAccount instance", async () => { - const derivator = poolEngine.derivator; - assertExists(derivator, "Pool engine should have a derivator"); - - // Create UTXOBasedAccount directly in the test with a balance fetching function - utxoAccount = new UtxoBasedStellarAccount({ - derivator, - root: testRoot, - options: { - batchSize: 10, - fetchBalances: async (publicKeys: Uint8Array[]) => { - return poolEngine.read({ - ...txInvocation, - method: ReadMethods.balances, - methodArgs: { - utxos: publicKeys.map((pk) => Buffer.from(pk)), - }, - }); - }, - }, - }); - - assertExists(utxoAccount, "UTXOBasedAccount should be initialized"); - }); - - // Derive a batch of UTXOs - await t.step("should derive a batch of UTXOs", async () => { - const batchSize = 5; - await utxoAccount.deriveBatch({ startIndex: 0, count: batchSize }); - - const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); - assertEquals( - freeUtxos.length, - batchSize, - "Should have derived the correct number of UTXOs" - ); - - // Verify each UTXO has required properties - for (const utxo of freeUtxos) { - assertExists(utxo.publicKey, "UTXO should have a public key"); - assertExists(utxo.privateKey, "UTXO should have a private key"); - - // Verify the keypair can sign data - const testData = new Uint8Array(32); - crypto.getRandomValues(testData); - const signature = await utxo.signPayload(testData); - assertExists(signature, "Should be able to generate a signature"); - } - }); - - // Deposit into a UTXO managed by the account - await t.step("should deposit to a UTXO and update its state", async () => { - // Get a free UTXO - const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); - - const testUtxo = freeUtxos[0]; - assertExists(testUtxo, "Should have at least one free UTXO"); - - // We know the UTXOs are indexed starting from 0, since we derived them that way - const utxoIndex = 0; - - // Deposit to the UTXO - const depositResult = (await poolEngine.write({ - ...txInvocation, - method: WriteMethods.deposit, - methodArgs: { - from: account.getPublicKey(), - amount: depositAmount, - utxo: Buffer.from(testUtxo.publicKey), - }, - options: { verboseOutput: true, includeHashOutput: true }, - })) as SorobanTransactionPipelineOutputVerbose; - - assertExists( - depositResult.sorobanTransactionOutput, - "Deposit transaction result should exist" - ); - - // Mark the UTXO as having a balance (would normally be done by batchLoad) - utxoAccount.updateUTXOState(utxoIndex, UTXOStatus.UNSPENT); - - // Verify the balance through the contract - const balanceResult = await poolEngine.read({ - ...txInvocation, - method: ReadMethods.balance, - methodArgs: { - utxo: Buffer.from(testUtxo.publicKey), - }, - }); - - assertEquals( - balanceResult, - depositAmount, - "UTXO balance should match the deposited amount" - ); - }); - - // Test batch loading of UTXOs - await t.step("should batch load UTXOs with balances", async () => { - // Load all UTXOs and check their states - await utxoAccount.batchLoad(); - - // Should have at least one UNSPENT UTXO - const unspentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); - assertEquals( - unspentUtxos.length >= 1, - true, - "Should have at least one UNSPENT UTXO after batch loading" - ); - - // Other UTXOs should be marked as SPENT or FREE - const allUtxos = [ - ...utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT), - ...utxoAccount.getUTXOsByState(UTXOStatus.SPENT), - ...utxoAccount.getUTXOsByState(UTXOStatus.FREE), - ]; - - // We derived 5 UTXOs, so all 5 should be accounted for - assertEquals(allUtxos.length, 5, "Should have accounted for all UTXOs"); - }); - - // Test withdrawing from an UNSPENT UTXO - await t.step( - "should withdraw from an UNSPENT UTXO and update its state", - async () => { - // Get an UNSPENT UTXO - const unspentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); - assertExists( - unspentUtxos.length > 0, - "Should have at least one UNSPENT UTXO" - ); - const testUtxo = unspentUtxos[0]; - - // For finding the index, we know we've been working with index 0 in previous steps - const utxoIndex = 0; - - // Generate withdraw payload - const withdrawPayload = poolEngine.buildWithdrawPayload({ - utxo: testUtxo.publicKey, - amount: depositAmount, - }); - - // Sign the payload - const signature = await testUtxo.signPayload(withdrawPayload); - assertExists(signature, "Should generate a valid signature"); - - // Execute withdrawal - const withdrawResult = (await poolEngine.write({ - ...txInvocation, - method: WriteMethods.withdraw, - methodArgs: { - to: account.getPublicKey(), - amount: depositAmount, - utxo: Buffer.from(testUtxo.publicKey), - signature: Buffer.from(signature), - }, - options: { verboseOutput: true, includeHashOutput: true }, - })) as SorobanTransactionPipelineOutputVerbose; - - assertExists( - withdrawResult.sorobanTransactionOutput, - "Withdraw transaction result should exist" - ); - - // Manually update the UTXO state to SPENT after withdrawal - utxoAccount.updateUTXOState(utxoIndex, UTXOStatus.SPENT); - - // Refresh UTXO states - await utxoAccount.batchLoad(); - - // Verify the balance is now zero - const balanceResult = await poolEngine.read({ - ...txInvocation, - method: ReadMethods.balance, - methodArgs: { - utxo: Buffer.from(testUtxo.publicKey), - }, - }); - - assertEquals( - balanceResult, - 0n, - "UTXO balance should be zero after withdrawal" - ); - - // Verify the UTXO is now in the SPENT collection - const spentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.SPENT); - const isTestUtxoSpent = spentUtxos.some( - (spentUtxo) => - Buffer.from(spentUtxo.publicKey).compare( - Buffer.from(testUtxo.publicKey) - ) === 0 - ); - - assertEquals( - isTestUtxoSpent, - true, - "The withdrawn UTXO should be marked as SPENT" - ); - } - ); - - // Test deriving additional UTXOs after the initial batch - await t.step("should derive additional UTXO batches", async () => { - const initialCount = [ - ...utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT), - ...utxoAccount.getUTXOsByState(UTXOStatus.SPENT), - ...utxoAccount.getUTXOsByState(UTXOStatus.FREE), - ].length; - - // Derive another batch starting from index 5 - await utxoAccount.deriveBatch({ startIndex: 5, count: 3 }); - - const newCount = [ - ...utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT), - ...utxoAccount.getUTXOsByState(UTXOStatus.SPENT), - ...utxoAccount.getUTXOsByState(UTXOStatus.FREE), - ].length; - - assertEquals(newCount, initialCount + 3, "Should have added 3 new UTXOs"); - - // New UTXOs should be in FREE state - const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); - assertEquals( - freeUtxos.length >= 3, - true, - "Should have at least 3 FREE UTXOs" - ); - }); - - // Test error cases - await t.step("should handle invalid operations correctly", async () => { - // Attempt to withdraw with invalid signature - const freeUtxo = utxoAccount.getUTXOsByState(UTXOStatus.FREE)[0]; - assertExists(freeUtxo, "Should have a free UTXO for testing"); - - // Try withdrawal without deposit - const invalidWithdrawPayload = poolEngine.buildWithdrawPayload({ - utxo: freeUtxo.publicKey, - amount: 10n, // <- different amount than the actual amount in the transaction - }); - - const signature = await freeUtxo.signPayload(invalidWithdrawPayload); - - try { - await poolEngine.write({ - ...txInvocation, - method: WriteMethods.withdraw, - methodArgs: { - to: account.getPublicKey(), - amount: 100000n, - utxo: Buffer.from(freeUtxo.publicKey), - signature: Buffer.from(signature), - }, - }); - throw new Error("Should have failed"); - } catch (error) { - assertExists(error, "Expected error for withdrawal without balance"); - } - - // Try zero amount deposit - // todo: Review contract behavior for zero amount deposits - - // try { - // await poolEngine.write({ - // ...txInvocation, - // method: WriteMethods.deposit, - // methodArgs: { - // from: account.getPublicKey(), - // amount: 0n, - // utxo: Buffer.from(freeUtxo.publicKey), - // }, - // }); - // throw new Error("Should have failed"); - // } catch (error) { - // assertExists(error, "Expected error for zero amount deposit"); - // } - }); - - // Test UTXO reservation functionality - await t.step("should handle UTXO reservations correctly", async () => { - // Clear any existing reservations - utxoAccount.releaseStaleReservations(0); - - // Derive some UTXOs for testing - await utxoAccount.deriveBatch({ count: 5 }); - await utxoAccount.batchLoad(); // Update states - - // Try reserving more UTXOs than available - const freeCount = utxoAccount.getUTXOsByState(UTXOStatus.FREE).length; - const tooManyReserved = utxoAccount.reserveUTXOs(freeCount + 5); - assertEquals( - tooManyReserved, - null, - "Should return null when requesting too many UTXOs" - ); - - // Reserve some UTXOs - const reservedCount = 2; - const reserved = utxoAccount.reserveUTXOs(reservedCount); - assertExists(reserved, "Should successfully reserve UTXOs"); - - assertEquals( - reserved.length, - reservedCount, - "Should reserve exactly the requested number of UTXOs" - ); - - // Verify these UTXOs are actually reserved - const reservedUTXOs = utxoAccount.getReservedUTXOs(); - assertEquals( - reservedUTXOs.length >= reservedCount, - true, - "Should track reserved UTXOs" - ); - - // Verify we can reserve more UTXOs - const secondReservation = utxoAccount.reserveUTXOs(1); - assertExists( - secondReservation, - "Should be able to reserve different UTXOs" - ); - }); - - // Test UTXO selection strategies - await t.step("should select UTXOs using different strategies", async () => { - // Create some unspent UTXOs with different balances - const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); - - // Check if we have enough free UTXOs - if (freeUtxos.length < 2) { - // Derive more UTXOs if needed - await utxoAccount.deriveBatch({ count: 5 }); - } - - const testUtxo1 = utxoAccount.getUTXOsByState(UTXOStatus.FREE)[0]; - const testUtxo2 = utxoAccount.getUTXOsByState(UTXOStatus.FREE)[1]; - assertExists(testUtxo1, "Should have a free UTXO for testing"); - assertExists(testUtxo2, "Should have another free UTXO for testing"); - - // Get indices for the test UTXOs - const index1 = Number(testUtxo1.index); - const index2 = Number(testUtxo2.index); - - // Deposit different amounts - await poolEngine.write({ - ...txInvocation, - method: WriteMethods.deposit, - methodArgs: { - from: account.getPublicKey(), - amount: 1000000n, - utxo: Buffer.from(testUtxo1.publicKey), - }, - }); - - await poolEngine.write({ - ...txInvocation, - method: WriteMethods.deposit, - methodArgs: { - from: account.getPublicKey(), - amount: 500000n, - utxo: Buffer.from(testUtxo2.publicKey), - }, - }); - - // Update UTXO states - utxoAccount.updateUTXOState(index1, UTXOStatus.UNSPENT, 1000000n); - utxoAccount.updateUTXOState(index2, UTXOStatus.UNSPENT, 500000n); - - // Check balances using contract to validate our updates - const balance1 = await poolEngine.read({ - ...txInvocation, - method: ReadMethods.balance, - methodArgs: { - utxo: Buffer.from(testUtxo1.publicKey), - }, - }); - - // Skip the actual test if balance verification fails - if (balance1 === 1000000n) { - // Test sequential selection - const sequentialResult = utxoAccount.selectUTXOsForTransfer(750000n); - assertExists(sequentialResult, "Should find UTXOs for transfer"); - assertEquals( - sequentialResult.selectedUTXOs.length >= 1, - true, - "Should select at least one UTXO when possible" - ); - } - }); - - // Test stale reservation release - await t.step("should release stale reservations", async () => { - // Clear any existing reservations - utxoAccount.releaseStaleReservations(0); - - // Reserve some UTXOs - const reserved = utxoAccount.reserveUTXOs(2); - assertExists(reserved, "Should successfully reserve UTXOs"); - - // Release stale reservations (all of them, since we're using a 0ms age) - const releasedCount = utxoAccount.releaseStaleReservations(0); - assertEquals(releasedCount, 2, "Should release all stale reservations"); - - // Verify they can be reserved again - const newReservation = utxoAccount.reserveUTXOs(2); - assertExists( - newReservation, - "Should be able to reserve previously stale UTXOs" - ); - }); -}); +// // deno-lint-ignore-file require-await +// import { assertEquals, assertExists } from "@std/assert"; +// import { Buffer } from "buffer"; +// import { +// type PoolEngine, +// ReadMethods, +// WriteMethods, +// UtxoBasedStellarAccount, +// UTXOStatus, +// } from "../../mod.ts"; +// import { createTestAccount } from "../helpers/create-test-account.ts"; +// import { createTxInvocation } from "../helpers/create-tx-invocation.ts"; +// import { deployPrivacyPool } from "../helpers/deploy-pool.ts"; +// import type { SorobanTransactionPipelineOutputVerbose } from "stellar-plus/lib/stellar-plus/core/pipelines/soroban-transaction/types"; + +// // Testnet XLM contract ID +// const XLM_CONTRACT_ID_TESTNET = +// "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; + +// Deno.test("UTXOBasedAccount Integration Tests", async (t) => { +// const { account, networkConfig } = await createTestAccount(); +// const txInvocation = createTxInvocation(account); +// let poolEngine: PoolEngine; +// let utxoAccount: UtxoBasedStellarAccount; +// const depositAmount = 500000n; // 0.05 XLM +// const testRoot = "S-TEST_SECRET_ROOT"; + +// // Setup test environment +// await t.step("setup: deploy privacy pool contract", async () => { +// poolEngine = await deployPrivacyPool({ +// admin: account, +// assetContractId: XLM_CONTRACT_ID_TESTNET, +// networkConfig, +// }); + +// assertExists(poolEngine, "Pool engine should be initialized"); +// assertExists(poolEngine.getContractId(), "Contract ID should be generated"); +// }); + +// // Initialize UTXOBasedAccount with pool engine's derivator - directly in the test +// await t.step("setup: create UTXOBasedAccount instance", async () => { +// const derivator = poolEngine.derivator; +// assertExists(derivator, "Pool engine should have a derivator"); + +// // Create UTXOBasedAccount directly in the test with a balance fetching function +// utxoAccount = new UtxoBasedStellarAccount({ +// derivator, +// root: testRoot, +// options: { +// batchSize: 10, +// fetchBalances: async (publicKeys: Uint8Array[]) => { +// return poolEngine.read({ +// ...txInvocation, +// method: ReadMethods.balances, +// methodArgs: { +// utxos: publicKeys.map((pk) => Buffer.from(pk)), +// }, +// }); +// }, +// }, +// }); + +// assertExists(utxoAccount, "UTXOBasedAccount should be initialized"); +// }); + +// // Derive a batch of UTXOs +// await t.step("should derive a batch of UTXOs", async () => { +// const batchSize = 5; +// await utxoAccount.deriveBatch({ startIndex: 0, count: batchSize }); + +// const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); +// assertEquals( +// freeUtxos.length, +// batchSize, +// "Should have derived the correct number of UTXOs" +// ); + +// // Verify each UTXO has required properties +// for (const utxo of freeUtxos) { +// assertExists(utxo.publicKey, "UTXO should have a public key"); +// assertExists(utxo.privateKey, "UTXO should have a private key"); + +// // Verify the keypair can sign data +// const testData = new Uint8Array(32); +// crypto.getRandomValues(testData); +// const signature = await utxo.signPayload(testData); +// assertExists(signature, "Should be able to generate a signature"); +// } +// }); + +// // Deposit into a UTXO managed by the account +// await t.step("should deposit to a UTXO and update its state", async () => { +// // Get a free UTXO +// const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); + +// const testUtxo = freeUtxos[0]; +// assertExists(testUtxo, "Should have at least one free UTXO"); + +// // We know the UTXOs are indexed starting from 0, since we derived them that way +// const utxoIndex = 0; + +// // Deposit to the UTXO +// const depositResult = (await poolEngine.write({ +// ...txInvocation, +// method: WriteMethods.deposit, +// methodArgs: { +// from: account.getPublicKey(), +// amount: depositAmount, +// utxo: Buffer.from(testUtxo.publicKey), +// }, +// options: { verboseOutput: true, includeHashOutput: true }, +// })) as SorobanTransactionPipelineOutputVerbose; + +// assertExists( +// depositResult.sorobanTransactionOutput, +// "Deposit transaction result should exist" +// ); + +// // Mark the UTXO as having a balance (would normally be done by batchLoad) +// utxoAccount.updateUTXOState(utxoIndex, UTXOStatus.UNSPENT); + +// // Verify the balance through the contract +// const balanceResult = await poolEngine.read({ +// ...txInvocation, +// method: ReadMethods.balance, +// methodArgs: { +// utxo: Buffer.from(testUtxo.publicKey), +// }, +// }); + +// assertEquals( +// balanceResult, +// depositAmount, +// "UTXO balance should match the deposited amount" +// ); +// }); + +// // Test batch loading of UTXOs +// await t.step("should batch load UTXOs with balances", async () => { +// // Load all UTXOs and check their states +// await utxoAccount.batchLoad(); + +// // Should have at least one UNSPENT UTXO +// const unspentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); +// assertEquals( +// unspentUtxos.length >= 1, +// true, +// "Should have at least one UNSPENT UTXO after batch loading" +// ); + +// // Other UTXOs should be marked as SPENT or FREE +// const allUtxos = [ +// ...utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT), +// ...utxoAccount.getUTXOsByState(UTXOStatus.SPENT), +// ...utxoAccount.getUTXOsByState(UTXOStatus.FREE), +// ]; + +// // We derived 5 UTXOs, so all 5 should be accounted for +// assertEquals(allUtxos.length, 5, "Should have accounted for all UTXOs"); +// }); + +// // Test withdrawing from an UNSPENT UTXO +// await t.step( +// "should withdraw from an UNSPENT UTXO and update its state", +// async () => { +// // Get an UNSPENT UTXO +// const unspentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); +// assertExists( +// unspentUtxos.length > 0, +// "Should have at least one UNSPENT UTXO" +// ); +// const testUtxo = unspentUtxos[0]; + +// // For finding the index, we know we've been working with index 0 in previous steps +// const utxoIndex = 0; + +// // Generate withdraw payload +// const withdrawPayload = poolEngine.buildWithdrawPayload({ +// utxo: testUtxo.publicKey, +// amount: depositAmount, +// }); + +// // Sign the payload +// const signature = await testUtxo.signPayload(withdrawPayload); +// assertExists(signature, "Should generate a valid signature"); + +// // Execute withdrawal +// const withdrawResult = (await poolEngine.write({ +// ...txInvocation, +// method: WriteMethods.withdraw, +// methodArgs: { +// to: account.getPublicKey(), +// amount: depositAmount, +// utxo: Buffer.from(testUtxo.publicKey), +// signature: Buffer.from(signature), +// }, +// options: { verboseOutput: true, includeHashOutput: true }, +// })) as SorobanTransactionPipelineOutputVerbose; + +// assertExists( +// withdrawResult.sorobanTransactionOutput, +// "Withdraw transaction result should exist" +// ); + +// // Manually update the UTXO state to SPENT after withdrawal +// utxoAccount.updateUTXOState(utxoIndex, UTXOStatus.SPENT); + +// // Refresh UTXO states +// await utxoAccount.batchLoad(); + +// // Verify the balance is now zero +// const balanceResult = await poolEngine.read({ +// ...txInvocation, +// method: ReadMethods.balance, +// methodArgs: { +// utxo: Buffer.from(testUtxo.publicKey), +// }, +// }); + +// assertEquals( +// balanceResult, +// 0n, +// "UTXO balance should be zero after withdrawal" +// ); + +// // Verify the UTXO is now in the SPENT collection +// const spentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.SPENT); +// const isTestUtxoSpent = spentUtxos.some( +// (spentUtxo) => +// Buffer.from(spentUtxo.publicKey).compare( +// Buffer.from(testUtxo.publicKey) +// ) === 0 +// ); + +// assertEquals( +// isTestUtxoSpent, +// true, +// "The withdrawn UTXO should be marked as SPENT" +// ); +// } +// ); + +// // Test deriving additional UTXOs after the initial batch +// await t.step("should derive additional UTXO batches", async () => { +// const initialCount = [ +// ...utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT), +// ...utxoAccount.getUTXOsByState(UTXOStatus.SPENT), +// ...utxoAccount.getUTXOsByState(UTXOStatus.FREE), +// ].length; + +// // Derive another batch starting from index 5 +// await utxoAccount.deriveBatch({ startIndex: 5, count: 3 }); + +// const newCount = [ +// ...utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT), +// ...utxoAccount.getUTXOsByState(UTXOStatus.SPENT), +// ...utxoAccount.getUTXOsByState(UTXOStatus.FREE), +// ].length; + +// assertEquals(newCount, initialCount + 3, "Should have added 3 new UTXOs"); + +// // New UTXOs should be in FREE state +// const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); +// assertEquals( +// freeUtxos.length >= 3, +// true, +// "Should have at least 3 FREE UTXOs" +// ); +// }); + +// // Test error cases +// await t.step("should handle invalid operations correctly", async () => { +// // Attempt to withdraw with invalid signature +// const freeUtxo = utxoAccount.getUTXOsByState(UTXOStatus.FREE)[0]; +// assertExists(freeUtxo, "Should have a free UTXO for testing"); + +// // Try withdrawal without deposit +// const invalidWithdrawPayload = poolEngine.buildWithdrawPayload({ +// utxo: freeUtxo.publicKey, +// amount: 10n, // <- different amount than the actual amount in the transaction +// }); + +// const signature = await freeUtxo.signPayload(invalidWithdrawPayload); + +// try { +// await poolEngine.write({ +// ...txInvocation, +// method: WriteMethods.withdraw, +// methodArgs: { +// to: account.getPublicKey(), +// amount: 100000n, +// utxo: Buffer.from(freeUtxo.publicKey), +// signature: Buffer.from(signature), +// }, +// }); +// throw new Error("Should have failed"); +// } catch (error) { +// assertExists(error, "Expected error for withdrawal without balance"); +// } + +// // Try zero amount deposit +// // todo: Review contract behavior for zero amount deposits + +// // try { +// // await poolEngine.write({ +// // ...txInvocation, +// // method: WriteMethods.deposit, +// // methodArgs: { +// // from: account.getPublicKey(), +// // amount: 0n, +// // utxo: Buffer.from(freeUtxo.publicKey), +// // }, +// // }); +// // throw new Error("Should have failed"); +// // } catch (error) { +// // assertExists(error, "Expected error for zero amount deposit"); +// // } +// }); + +// // Test UTXO reservation functionality +// await t.step("should handle UTXO reservations correctly", async () => { +// // Clear any existing reservations +// utxoAccount.releaseStaleReservations(0); + +// // Derive some UTXOs for testing +// await utxoAccount.deriveBatch({ count: 5 }); +// await utxoAccount.batchLoad(); // Update states + +// // Try reserving more UTXOs than available +// const freeCount = utxoAccount.getUTXOsByState(UTXOStatus.FREE).length; +// const tooManyReserved = utxoAccount.reserveUTXOs(freeCount + 5); +// assertEquals( +// tooManyReserved, +// null, +// "Should return null when requesting too many UTXOs" +// ); + +// // Reserve some UTXOs +// const reservedCount = 2; +// const reserved = utxoAccount.reserveUTXOs(reservedCount); +// assertExists(reserved, "Should successfully reserve UTXOs"); + +// assertEquals( +// reserved.length, +// reservedCount, +// "Should reserve exactly the requested number of UTXOs" +// ); + +// // Verify these UTXOs are actually reserved +// const reservedUTXOs = utxoAccount.getReservedUTXOs(); +// assertEquals( +// reservedUTXOs.length >= reservedCount, +// true, +// "Should track reserved UTXOs" +// ); + +// // Verify we can reserve more UTXOs +// const secondReservation = utxoAccount.reserveUTXOs(1); +// assertExists( +// secondReservation, +// "Should be able to reserve different UTXOs" +// ); +// }); + +// // Test UTXO selection strategies +// await t.step("should select UTXOs using different strategies", async () => { +// // Create some unspent UTXOs with different balances +// const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); + +// // Check if we have enough free UTXOs +// if (freeUtxos.length < 2) { +// // Derive more UTXOs if needed +// await utxoAccount.deriveBatch({ count: 5 }); +// } + +// const testUtxo1 = utxoAccount.getUTXOsByState(UTXOStatus.FREE)[0]; +// const testUtxo2 = utxoAccount.getUTXOsByState(UTXOStatus.FREE)[1]; +// assertExists(testUtxo1, "Should have a free UTXO for testing"); +// assertExists(testUtxo2, "Should have another free UTXO for testing"); + +// // Get indices for the test UTXOs +// const index1 = Number(testUtxo1.index); +// const index2 = Number(testUtxo2.index); + +// // Deposit different amounts +// await poolEngine.write({ +// ...txInvocation, +// method: WriteMethods.deposit, +// methodArgs: { +// from: account.getPublicKey(), +// amount: 1000000n, +// utxo: Buffer.from(testUtxo1.publicKey), +// }, +// }); + +// await poolEngine.write({ +// ...txInvocation, +// method: WriteMethods.deposit, +// methodArgs: { +// from: account.getPublicKey(), +// amount: 500000n, +// utxo: Buffer.from(testUtxo2.publicKey), +// }, +// }); + +// // Update UTXO states +// utxoAccount.updateUTXOState(index1, UTXOStatus.UNSPENT, 1000000n); +// utxoAccount.updateUTXOState(index2, UTXOStatus.UNSPENT, 500000n); + +// // Check balances using contract to validate our updates +// const balance1 = await poolEngine.read({ +// ...txInvocation, +// method: ReadMethods.balance, +// methodArgs: { +// utxo: Buffer.from(testUtxo1.publicKey), +// }, +// }); + +// // Skip the actual test if balance verification fails +// if (balance1 === 1000000n) { +// // Test sequential selection +// const sequentialResult = utxoAccount.selectUTXOsForTransfer(750000n); +// assertExists(sequentialResult, "Should find UTXOs for transfer"); +// assertEquals( +// sequentialResult.selectedUTXOs.length >= 1, +// true, +// "Should select at least one UTXO when possible" +// ); +// } +// }); + +// // Test stale reservation release +// await t.step("should release stale reservations", async () => { +// // Clear any existing reservations +// utxoAccount.releaseStaleReservations(0); + +// // Reserve some UTXOs +// const reserved = utxoAccount.reserveUTXOs(2); +// assertExists(reserved, "Should successfully reserve UTXOs"); + +// // Release stale reservations (all of them, since we're using a 0ms age) +// const releasedCount = utxoAccount.releaseStaleReservations(0); +// assertEquals(releasedCount, 2, "Should release all stale reservations"); + +// // Verify they can be reserved again +// const newReservation = utxoAccount.reserveUTXOs(2); +// assertExists( +// newReservation, +// "Should be able to reserve previously stale UTXOs" +// ); +// }); +// }); diff --git a/test/scripts/full-setup.ts b/test/scripts/full-setup.ts index 6b03e16..e1abfc0 100644 --- a/test/scripts/full-setup.ts +++ b/test/scripts/full-setup.ts @@ -1,98 +1,98 @@ -import { StellarPlus } from "stellar-plus"; -import { deployPrivacyPool } from "../helpers/deploy-pool.ts"; -import { traverseObjectLog } from "../utils/traverse-object-log.ts"; -import { ReadMethods, WriteMethods } from "../../mod.ts"; -import { deriveP256KeyPairFromSeed } from "../../src/utils/secp256r1/deriveP256KeyPairFromSeed.ts"; -import { Buffer } from "buffer"; -import { signPayload } from "../../src/utils/secp256r1/signPayload.ts"; - -const { DefaultAccountHandler } = StellarPlus.Account; -const { SACHandler } = StellarPlus.Asset; -const { TestNet } = StellarPlus.Network; - -export const TEST_NETWORK = TestNet(); - -export const admin = new DefaultAccountHandler({ - networkConfig: TEST_NETWORK, -}); - -const XLM_CONTRACT_ID_TESTNET = - "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; - -const XLM_CONTRACT_ID = - TEST_NETWORK.name === TestNet().name ? XLM_CONTRACT_ID_TESTNET : ""; - -const XLM = new SACHandler({ - networkConfig: TEST_NETWORK, - code: "XLM", - contractParameters: { - contractId: XLM_CONTRACT_ID, - }, -}); - -const txInvocation = { - header: { - source: admin.getPublicKey(), - fee: "100000", - timeout: 30, - }, - signers: [admin], -}; - -const runSetup = async () => { - console.log("Initializing admin: ", admin.getPublicKey()); - await admin.initializeWithFriendbot(); - console.log( - "Admin initialized with balance: ", - await XLM.classicHandler.balance(admin.getPublicKey()) - ); - - const pool = await deployPrivacyPool({ - admin: admin, - assetContractId: XLM_CONTRACT_ID, - networkConfig: TEST_NETWORK, - }); - - // console.log( - // "admin: ", - // await pool.read({ - // ...txInvocation, - // method: ReadMethods.admin, - // methodArgs: {}, - // }) - // ); - - // const utxoKP = deriveP256KeyPairFromSeed(Buffer.from("TEST")); - - // await pool.write({ - // ...txInvocation, - // method: WriteMethods.deposit, - // methodArgs: { - // amount: 5000n, - // from: admin.getPublicKey(), - // utxo: utxoKP.publicKey as Buffer, - // }, - // }); - - // const pyaload = pool.buildBurnPayload({ - // utxo: utxoKP.publicKey as Buffer, - // amount: 5000n, - // }); - - // const signedPayload = await signPayload(pyaload, utxoKP.privateKey as Buffer); - - // await pool.write({ - // ...txInvocation, - // method: WriteMethods.withdraw, - // methodArgs: { - // utxo: utxoKP.publicKey as Buffer, - // to: admin.getPublicKey(), - // signature: signedPayload as Buffer, - // amount: 5000n, - // }, - // }); -}; - -runSetup() - .catch((e) => traverseObjectLog(e, { maxDepth: 4, nodeThreshold: 7 })) - .then(() => console.log("Setup complete!")); +// import { StellarPlus } from "stellar-plus"; +// import { deployPrivacyPool } from "../helpers/deploy-pool.ts"; +// import { traverseObjectLog } from "../utils/traverse-object-log.ts"; +// import { ReadMethods, WriteMethods } from "../../mod.ts"; +// import { deriveP256KeyPairFromSeed } from "../../src/utils/secp256r1/deriveP256KeyPairFromSeed.ts"; +// import { Buffer } from "buffer"; +// import { signPayload } from "../../src/utils/secp256r1/signPayload.ts"; + +// const { DefaultAccountHandler } = StellarPlus.Account; +// const { SACHandler } = StellarPlus.Asset; +// const { TestNet } = StellarPlus.Network; + +// export const TEST_NETWORK = TestNet(); + +// export const admin = new DefaultAccountHandler({ +// networkConfig: TEST_NETWORK, +// }); + +// const XLM_CONTRACT_ID_TESTNET = +// "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; + +// const XLM_CONTRACT_ID = +// TEST_NETWORK.name === TestNet().name ? XLM_CONTRACT_ID_TESTNET : ""; + +// const XLM = new SACHandler({ +// networkConfig: TEST_NETWORK, +// code: "XLM", +// contractParameters: { +// contractId: XLM_CONTRACT_ID, +// }, +// }); + +// const txInvocation = { +// header: { +// source: admin.getPublicKey(), +// fee: "100000", +// timeout: 30, +// }, +// signers: [admin], +// }; + +// const runSetup = async () => { +// console.log("Initializing admin: ", admin.getPublicKey()); +// await admin.initializeWithFriendbot(); +// console.log( +// "Admin initialized with balance: ", +// await XLM.classicHandler.balance(admin.getPublicKey()) +// ); + +// const pool = await deployPrivacyPool({ +// admin: admin, +// assetContractId: XLM_CONTRACT_ID, +// networkConfig: TEST_NETWORK, +// }); + +// // console.log( +// // "admin: ", +// // await pool.read({ +// // ...txInvocation, +// // method: ReadMethods.admin, +// // methodArgs: {}, +// // }) +// // ); + +// // const utxoKP = deriveP256KeyPairFromSeed(Buffer.from("TEST")); + +// // await pool.write({ +// // ...txInvocation, +// // method: WriteMethods.deposit, +// // methodArgs: { +// // amount: 5000n, +// // from: admin.getPublicKey(), +// // utxo: utxoKP.publicKey as Buffer, +// // }, +// // }); + +// // const pyaload = pool.buildBurnPayload({ +// // utxo: utxoKP.publicKey as Buffer, +// // amount: 5000n, +// // }); + +// // const signedPayload = await signPayload(pyaload, utxoKP.privateKey as Buffer); + +// // await pool.write({ +// // ...txInvocation, +// // method: WriteMethods.withdraw, +// // methodArgs: { +// // utxo: utxoKP.publicKey as Buffer, +// // to: admin.getPublicKey(), +// // signature: signedPayload as Buffer, +// // amount: 5000n, +// // }, +// // }); +// }; + +// runSetup() +// .catch((e) => traverseObjectLog(e, { maxDepth: 4, nodeThreshold: 7 })) +// .then(() => console.log("Setup complete!")); diff --git a/test/utils/traverse-object-log.ts b/test/utils/traverse-object-log.ts index cb0416b..e9c4302 100644 --- a/test/utils/traverse-object-log.ts +++ b/test/utils/traverse-object-log.ts @@ -1,4 +1,5 @@ export function traverseObjectLog( + // deno-lint-ignore no-explicit-any obj: any, options: { maxDepth?: number; nodeThreshold?: number } = {}, currentDepth: number = 0 From 0ba4430c02424db960e264ad25628204254e7c54 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Tue, 21 Oct 2025 09:03:51 -0300 Subject: [PATCH 51/90] refactor: update types and imports across multiple files for improved clarity and consistency - Changed `publicKey` type in `IUTXOKeypairBase` to `UTXOPublicKey` in `src/core/utxo-keypair-base/types.ts`. - Updated import statements to use `type` for `BaseDerivator` in `src/core/utxo-keypair/index.ts`. - Modified `fetchBalance` method to accept `UTXOPublicKey` in `src/core/utxo-keypair/types.ts`. - Refactored test imports to use `@std/assert` in `src/core/utxo-keypair/index.unit.test.ts` and `src/derivation/base/index.unit.test.ts`. - Commented out unused code in `src/transaction-builder/xdr/ops-to-xdr.ts` for cleaner codebase. - Updated `generateBundleAuthEntry` and `generateDepositAuthEntry` to use `xdrHelper` in `src/utils/auth/bundle-auth-entry.ts` and `src/utils/auth/deposit-auth-entry.ts`. - Changed secret key type in `StellarDerivator` methods to `Ed25519SecretKey` in `src/derivation/stellar/index.ts`. - Updated UTXOKeypair import to use `type` in `src/utxo-based-account/types.ts`. --- src/core/utxo-keypair-base/types.ts | 4 +- src/core/utxo-keypair/index.ts | 2 +- src/core/utxo-keypair/index.unit.test.ts | 8 +- src/core/utxo-keypair/types.ts | 7 +- src/derivation/base/index.unit.test.ts | 6 +- src/derivation/stellar/index.ts | 15 ++-- src/derivation/stellar/index.unit.test.ts | 5 +- src/derivation/stellar/types.ts | 10 +-- src/transaction-builder/xdr/ops-to-xdr.ts | 80 +++++++++---------- src/utils/auth/bundle-auth-entry.ts | 8 +- src/utils/auth/deposit-auth-entry.ts | 12 +-- .../secp256r1/deriveP256KeyPairFromSeed.ts | 11 ++- src/utxo-based-account/index.unit.test.ts | 2 +- src/utxo-based-account/types.ts | 2 +- 14 files changed, 80 insertions(+), 92 deletions(-) diff --git a/src/core/utxo-keypair-base/types.ts b/src/core/utxo-keypair-base/types.ts index 152b468..84488c2 100644 --- a/src/core/utxo-keypair-base/types.ts +++ b/src/core/utxo-keypair-base/types.ts @@ -1,6 +1,8 @@ export interface IUTXOKeypairBase { privateKey: Uint8Array; - publicKey: Uint8Array; + publicKey: UTXOPublicKey; signPayload(payload: Uint8Array): Promise; } + +export type UTXOPublicKey = Uint8Array; diff --git a/src/core/utxo-keypair/index.ts b/src/core/utxo-keypair/index.ts index 8815da5..b55f7bf 100644 --- a/src/core/utxo-keypair/index.ts +++ b/src/core/utxo-keypair/index.ts @@ -1,6 +1,6 @@ import { signPayload } from "../../utils/secp256r1/signPayload.ts"; import { UTXOKeypairBase } from "../utxo-keypair-base/index.ts"; -import { BaseDerivator } from "../../derivation/base/index.ts"; +import type { BaseDerivator } from "../../derivation/base/index.ts"; import { type BalanceFetcher, type IUTXOKeypair, diff --git a/src/core/utxo-keypair/index.unit.test.ts b/src/core/utxo-keypair/index.unit.test.ts index e584547..5ca1aa4 100644 --- a/src/core/utxo-keypair/index.unit.test.ts +++ b/src/core/utxo-keypair/index.unit.test.ts @@ -1,9 +1,5 @@ // deno-lint-ignore-file require-await -import { - assertEquals, - assertNotEquals, - assertRejects, -} from "https://deno.land/std@0.220.1/assert/mod.ts"; +import { assertEquals, assertNotEquals, assertRejects } from "@std/assert"; import { BaseDerivator } from "../../derivation/base/index.ts"; import { UTXOKeypair } from "./index.ts"; import { type BalanceFetcher, UTXOStatus } from "./types.ts"; @@ -190,6 +186,7 @@ Deno.test("UTXOKeypair", async (t) => { assertEquals(utxo.index, "0"); // The root should not be stored in the UTXOKeypair + // deno-lint-ignore no-explicit-any assertEquals((utxo as any).root, undefined); } ); @@ -228,6 +225,7 @@ Deno.test("UTXOKeypair", async (t) => { assertEquals(utxo.context, "test-context"); // The root should not be stored in any of the UTXOKeypairs + // deno-lint-ignore no-explicit-any assertEquals((utxo as any).root, undefined); } } diff --git a/src/core/utxo-keypair/types.ts b/src/core/utxo-keypair/types.ts index 609ea54..77be8f9 100644 --- a/src/core/utxo-keypair/types.ts +++ b/src/core/utxo-keypair/types.ts @@ -1,4 +1,7 @@ -import type { IUTXOKeypairBase } from "../utxo-keypair-base/types.ts"; +import type { + IUTXOKeypairBase, + UTXOPublicKey, +} from "../utxo-keypair-base/types.ts"; /** * Interface for fetching balance information for UTXOs @@ -10,7 +13,7 @@ export interface BalanceFetcher { * @param publicKey - The public key to fetch balance for * @returns Promise resolving to the balance as a bigint */ - fetchBalance(publicKey: Uint8Array): Promise; + fetchBalance(publicKey: UTXOPublicKey): Promise; } export interface IUTXOKeypair< diff --git a/src/derivation/base/index.unit.test.ts b/src/derivation/base/index.unit.test.ts index 0e83c03..524b52b 100644 --- a/src/derivation/base/index.unit.test.ts +++ b/src/derivation/base/index.unit.test.ts @@ -1,9 +1,5 @@ import { BaseDerivator, generatePlainTextSeed, hashSeed } from "./index.ts"; -import { - assertEquals, - assertThrows, - assertExists, -} from "https://deno.land/std@0.220.1/assert/mod.ts"; +import { assertEquals, assertThrows, assertExists } from "@std/assert"; Deno.test("BaseDerivator", async (t) => { await t.step("assembleSeed should work with complete components", () => { diff --git a/src/derivation/stellar/index.ts b/src/derivation/stellar/index.ts index 1e248de..fd154a3 100644 --- a/src/derivation/stellar/index.ts +++ b/src/derivation/stellar/index.ts @@ -6,10 +6,7 @@ import type { } from "./types.ts"; import { BaseDerivator } from "../base/index.ts"; import type { StellarNetworkId } from "./stellar-network-id.ts"; -import type { - StellarSecretKey, - StellarSmartContractId, -} from "../../utils/types/stellar.types.ts"; +import type { ContractId, Ed25519SecretKey } from "@colibri/core"; /** * Assembles a network context string from network ID and smart contract ID @@ -19,7 +16,7 @@ import type { */ export function assembleNetworkContext( network: StellarNetworkId, - contractId: StellarSmartContractId + contractId: ContractId ): StellarNetworkContext { return `${network}${contractId}`; } @@ -41,7 +38,7 @@ export class StellarDerivator extends BaseDerivator< */ withNetworkAndContract( network: StellarNetworkId, - contractId: StellarSmartContractId + contractId: ContractId ): this { const context = assembleNetworkContext(network, contractId); return this.withContext(context); @@ -52,7 +49,7 @@ export class StellarDerivator extends BaseDerivator< * @param secretKey - Stellar secret key (starts with 'S') * @throws If root has already been set */ - withSecretKey(secretKey: StellarSecretKey): this { + withSecretKey(secretKey: Ed25519SecretKey): this { return this.withRoot(secretKey); } } @@ -66,8 +63,8 @@ export class StellarDerivator extends BaseDerivator< */ export function createForAccount( networkId: StellarNetworkId, - contractId: StellarSmartContractId, - secretKey: StellarSecretKey + contractId: ContractId, + secretKey: Ed25519SecretKey ): StellarDerivator { return new StellarDerivator() .withNetworkAndContract(networkId, contractId) diff --git a/src/derivation/stellar/index.unit.test.ts b/src/derivation/stellar/index.unit.test.ts index 7cd57df..37bf3da 100644 --- a/src/derivation/stellar/index.unit.test.ts +++ b/src/derivation/stellar/index.unit.test.ts @@ -4,10 +4,7 @@ import { createForAccount, } from "./index.ts"; import { StellarNetworkId } from "./stellar-network-id.ts"; -import { - assertEquals, - assertExists, -} from "https://deno.land/std@0.220.1/assert/mod.ts"; +import { assertEquals, assertExists } from "@std/assert"; // Constants for testing const TEST_SECRET_KEY = diff --git a/src/derivation/stellar/types.ts b/src/derivation/stellar/types.ts index f10df20..4245852 100644 --- a/src/derivation/stellar/types.ts +++ b/src/derivation/stellar/types.ts @@ -1,12 +1,9 @@ -import { - StellarSecretKey, - StellarSmartContractId, -} from "../../utils/types/stellar.types.ts"; +import type { ContractId, Ed25519SecretKey } from "@colibri/core"; import type { PlainDerivationSeed, SequenceIndex } from "../base/types.ts"; import type { StellarNetworkId } from "./stellar-network-id.ts"; export type StellarDerivationContext = StellarNetworkContext; -export type StellarDerivationRoot = StellarSecretKey; +export type StellarDerivationRoot = Ed25519SecretKey; export type StellarDerivationIndex = SequenceIndex; export type StellarDerivationSeed = PlainDerivationSeed< @@ -18,5 +15,4 @@ export type StellarDerivationSeed = PlainDerivationSeed< // Stellar Network Context combines the following: // - NetworkId: Passphrase of the network. // - SmartContractId: The smart contract id. Starts with the prefix C. -export type StellarNetworkContext = - `${StellarNetworkId}${StellarSmartContractId}`; +export type StellarNetworkContext = `${StellarNetworkId}${ContractId}`; diff --git a/src/transaction-builder/xdr/ops-to-xdr.ts b/src/transaction-builder/xdr/ops-to-xdr.ts index 5268bd8..adc256c 100644 --- a/src/transaction-builder/xdr/ops-to-xdr.ts +++ b/src/transaction-builder/xdr/ops-to-xdr.ts @@ -1,44 +1,44 @@ -import { xdr, nativeToScVal } from "@stellar/stellar-sdk"; -import { Buffer } from "buffer"; -import type { - CreateOperation, - DepositOperation, - WithdrawOperation, - SpendOperation, -} from "../types.ts"; +// import { xdr, nativeToScVal } from "@stellar/stellar-sdk"; +// import { Buffer } from "buffer"; +// import type { +// CreateOperation, +// DepositOperation, +// WithdrawOperation, +// SpendOperation, +// } from "../types.ts"; -export const createOpToXDR = (op: CreateOperation): xdr.ScVal => { - return xdr.ScVal.scvVec([ - xdr.ScVal.scvBytes(Buffer.from(op.utxo as Uint8Array)), - nativeToScVal(op.amount, { type: "i128" }), - ]); -}; +// export const createOpToXDR = (op: CreateOperation): xdr.ScVal => { +// return xdr.ScVal.scvVec([ +// xdr.ScVal.scvBytes(Buffer.from(op.utxo as Uint8Array)), +// nativeToScVal(op.amount, { type: "i128" }), +// ]); +// }; -export const depositOpToXDR = (op: DepositOperation): xdr.ScVal => { - return xdr.ScVal.scvVec([ - nativeToScVal(op.pubKey, { type: "address" }), - nativeToScVal(op.amount, { type: "i128" }), - op.conditions.length === 0 - ? xdr.ScVal.scvVec(null) - : xdr.ScVal.scvVec(op.conditions.map((c) => c.toScVal())), - ]); -}; +// export const depositOpToXDR = (op: DepositOperation): xdr.ScVal => { +// return xdr.ScVal.scvVec([ +// nativeToScVal(op.pubKey, { type: "address" }), +// nativeToScVal(op.amount, { type: "i128" }), +// op.conditions.length === 0 +// ? xdr.ScVal.scvVec(null) +// : xdr.ScVal.scvVec(op.conditions.map((c) => c.toScVal())), +// ]); +// }; -export const withdrawOpToXDR = (op: WithdrawOperation): xdr.ScVal => { - return xdr.ScVal.scvVec([ - nativeToScVal(op.pubKey, { type: "address" }), - nativeToScVal(op.amount, { type: "i128" }), - op.conditions.length === 0 - ? xdr.ScVal.scvVec(null) - : xdr.ScVal.scvVec(op.conditions.map((c) => c.toScVal())), - ]); -}; +// export const withdrawOpToXDR = (op: WithdrawOperation): xdr.ScVal => { +// return xdr.ScVal.scvVec([ +// nativeToScVal(op.pubKey, { type: "address" }), +// nativeToScVal(op.amount, { type: "i128" }), +// op.conditions.length === 0 +// ? xdr.ScVal.scvVec(null) +// : xdr.ScVal.scvVec(op.conditions.map((c) => c.toScVal())), +// ]); +// }; -export const spendOpToXDR = (op: SpendOperation): xdr.ScVal => { - return xdr.ScVal.scvVec([ - xdr.ScVal.scvBytes(Buffer.from(op.utxo as Uint8Array)), - op.conditions.length === 0 - ? xdr.ScVal.scvVec(null) - : xdr.ScVal.scvVec(op.conditions.map((c) => c.toScVal())), - ]); -}; +// export const spendOpToXDR = (op: SpendOperation): xdr.ScVal => { +// return xdr.ScVal.scvVec([ +// xdr.ScVal.scvBytes(Buffer.from(op.utxo as Uint8Array)), +// op.conditions.length === 0 +// ? xdr.ScVal.scvVec(null) +// : xdr.ScVal.scvVec(op.conditions.map((c) => c.toScVal())), +// ]); +// }; diff --git a/src/utils/auth/bundle-auth-entry.ts b/src/utils/auth/bundle-auth-entry.ts index f25933d..72c2a7b 100644 --- a/src/utils/auth/bundle-auth-entry.ts +++ b/src/utils/auth/bundle-auth-entry.ts @@ -1,5 +1,5 @@ -import { xdr } from "@stellar/stellar-sdk"; -import { type InvocationParams, paramsToAuthEntry } from "./auth-entries.ts"; +import type { xdr } from "@stellar/stellar-sdk"; +import { xdr as xdrHelper } from "@colibri/core"; export const generateBundleAuthEntry = ({ channelId, @@ -23,9 +23,9 @@ export const generateBundleAuthEntry = ({ args, }, subInvocations: [], - } as InvocationParams; + } as xdrHelper.InvocationParams; - const entry = paramsToAuthEntry({ + const entry = xdrHelper.paramsToAuthEntry({ credentials: { address: authId, nonce, diff --git a/src/utils/auth/deposit-auth-entry.ts b/src/utils/auth/deposit-auth-entry.ts index 5294207..c70e289 100644 --- a/src/utils/auth/deposit-auth-entry.ts +++ b/src/utils/auth/deposit-auth-entry.ts @@ -1,5 +1,5 @@ -import { xdr } from "@stellar/stellar-sdk"; -import { FnArg, InvocationParams, paramsToAuthEntry } from "./auth-entries.ts"; +import type { xdr } from "@stellar/stellar-sdk"; +import { xdr as xdrHelper } from "@colibri/core"; export const generateDepositAuthEntry = ({ channelId, @@ -33,14 +33,14 @@ export const generateDepositAuthEntry = ({ { value: depositor, type: "address" }, { value: channelId, type: "address" }, { value: amount, type: "i128" }, - ] as FnArg[], + ] as xdrHelper.FnArg[], }, subInvocations: [], - } as InvocationParams, + } as xdrHelper.InvocationParams, ], - } as InvocationParams; + } as xdrHelper.InvocationParams; - const entry = paramsToAuthEntry({ + const entry = xdrHelper.paramsToAuthEntry({ credentials: { address: depositor, nonce, diff --git a/src/utils/secp256r1/deriveP256KeyPairFromSeed.ts b/src/utils/secp256r1/deriveP256KeyPairFromSeed.ts index 826f3f1..190f328 100644 --- a/src/utils/secp256r1/deriveP256KeyPairFromSeed.ts +++ b/src/utils/secp256r1/deriveP256KeyPairFromSeed.ts @@ -1,11 +1,10 @@ -import { p256 } from "jsr:@noble/curves@^1.7.1/p256"; -import { sha256 } from "jsr:@noble/hashes@^1.7.1/sha256"; -import { hkdf } from "jsr:@noble/hashes@^1.7.1/hkdf"; -import { mapHashToField } from "jsr:@noble/curves@^1.7.1/abstract/modular"; +import { p256 } from "@noble/curves/p256"; +import { sha256 } from "@noble/hashes/sha256"; +import { hkdf } from "@noble/hashes/hkdf"; +import { mapHashToField } from "@noble/curves/abstract/modular"; import { bytesToBigIntBE } from "../conversion/bytesToBigIntBE.ts"; import { numberToBytesBE } from "../conversion/numberToBytesBE.ts"; -import { UTXOKeypairBase } from "../../core/utxo-keypair-base/index.ts"; import { encodePKCS8 } from "./encodePKCS8.ts"; /** @@ -36,5 +35,5 @@ export async function deriveP256KeyPairFromSeed( const pkcs8PrivateKey = encodePKCS8(rawPrivateKey, publicKey); // Return keypair components directly instead of creating a UTXOKeypair instance - return { privateKey: pkcs8PrivateKey, publicKey }; + return await { privateKey: pkcs8PrivateKey, publicKey }; } diff --git a/src/utxo-based-account/index.unit.test.ts b/src/utxo-based-account/index.unit.test.ts index 1e7894d..3160cb1 100644 --- a/src/utxo-based-account/index.unit.test.ts +++ b/src/utxo-based-account/index.unit.test.ts @@ -7,7 +7,7 @@ import { assertThrows, assertRejects, assertExists, -} from "https://deno.land/std@0.207.0/assert/mod.ts"; +} from "@std/assert"; import { UtxoBasedAccount } from "./index.ts"; import { UTXOStatus } from "../core/utxo-keypair/types.ts"; import { StellarDerivator } from "../derivation/stellar/index.ts"; diff --git a/src/utxo-based-account/types.ts b/src/utxo-based-account/types.ts index 39a678b..7846198 100644 --- a/src/utxo-based-account/types.ts +++ b/src/utxo-based-account/types.ts @@ -1,4 +1,4 @@ -import { UTXOKeypair } from "../core/utxo-keypair/index.ts"; +import type { UTXOKeypair } from "../core/utxo-keypair/index.ts"; /** * Result of UTXO selection for transfers From bcdf238a29b789a944da558d147ed87e8fcb6ab5 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Tue, 21 Oct 2025 09:04:11 -0300 Subject: [PATCH 52/90] feat: implement MoonlightOperation class with UTXO operation handling - Added `MoonlightOperation` class in `src/operation/index.ts` to manage UTXO operations including Create, Deposit, Withdraw, and Spend. - Introduced factory methods for creating instances of each operation type. - Implemented getter methods for retrieving operation details and conditions. - Defined interfaces for each operation type in `src/operation/types.ts` to ensure type safety and clarity. - Enhanced error handling for invalid operations and conditions. --- src/operation/index.ts | 448 ++++ src/operation/types.ts | 62 + src/privacy-channel/index.ts | 2 +- src/transaction-builder/index.ts | 449 +++- src/transaction-builder/index.unit.test.ts | 2206 ++++++----------- src/transaction-builder/utils/ordering.ts | 6 +- .../validators/operations.ts | 40 +- .../privacy-channel.integration.test.ts | 17 +- 8 files changed, 1629 insertions(+), 1601 deletions(-) create mode 100644 src/operation/index.ts create mode 100644 src/operation/types.ts diff --git a/src/operation/index.ts b/src/operation/index.ts new file mode 100644 index 0000000..150393b --- /dev/null +++ b/src/operation/index.ts @@ -0,0 +1,448 @@ +import { type Ed25519PublicKey, StrKey } from "@colibri/core"; +import type { Condition as ConditionType } from "../conditions/types.ts"; +import { UTXOOperationType } from "./types.ts"; +import type { + BaseOperation, + CreateOperation, + DepositOperation, + SpendOperation, + WithdrawOperation, +} from "./types.ts"; + +import { Condition } from "../conditions/index.ts"; +import { nativeToScVal, xdr } from "@stellar/stellar-sdk"; +import { Buffer } from "node:buffer"; +import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; + +export class MoonlightOperation implements BaseOperation { + private _op: UTXOOperationType; + private _amount?: bigint; + private _publicKey?: Ed25519PublicKey; + private _utxo?: UTXOPublicKey; + private _conditions?: ConditionType[]; + + private constructor({ + op, + amount, + publicKey, + utxo, + }: { + op: UTXOOperationType; + amount?: bigint; + publicKey?: Ed25519PublicKey; + utxo?: UTXOPublicKey; + }) { + if (amount !== undefined && amount <= 0n) { + throw new Error("Amount must be greater than zero"); + } + + // Only Create operations can't have conditions + if (op !== UTXOOperationType.CREATE) this.setConditions([]); + + this._op = op; + this._amount = amount; + this._publicKey = publicKey; + this._utxo = utxo; + } + + static create(utxo: UTXOPublicKey, amount: bigint): CreateOperation { + return new MoonlightOperation({ + op: UTXOOperationType.CREATE, + utxo, + amount, + }) as CreateOperation; + } + + static deposit( + publicKey: Ed25519PublicKey, + amount: bigint + ): DepositOperation { + if (!StrKey.isValidEd25519PublicKey(publicKey)) { + throw new Error("Invalid Ed25519 public key"); + } + + return new MoonlightOperation({ + op: UTXOOperationType.DEPOSIT, + publicKey, + amount, + }) as DepositOperation; + } + + static withdraw( + publicKey: Ed25519PublicKey, + amount: bigint + ): WithdrawOperation { + if (!StrKey.isValidEd25519PublicKey(publicKey)) { + throw new Error("Invalid Ed25519 public key."); + } + return new MoonlightOperation({ + op: UTXOOperationType.WITHDRAW, + publicKey, + amount, + }) as WithdrawOperation; + } + + static spend(utxo: UTXOPublicKey): SpendOperation { + return new MoonlightOperation({ + op: UTXOOperationType.SPEND, + utxo, + }) as SpendOperation; + } + + //========================================== + // Meta Requirement Methods + //========================================== + + /** + * Internal helper method to safely retrieve required properties. + * Uses method overloading to provide type-safe access to private fields. + * + * @param arg - The name of the property to retrieve + * @returns The value of the requested property + * @throws {Error} If the requested property is not set + * @private + */ + private require(arg: "_op"): UTXOOperationType; + private require(arg: "_amount"): bigint; + private require(arg: "_publicKey"): Ed25519PublicKey; + private require(arg: "_utxo"): UTXOPublicKey; + private require(arg: "_conditions"): ConditionType[]; + + private require( + arg: "_op" | "_amount" | "_publicKey" | "_utxo" | "_conditions" + ): + | UTXOOperationType + | bigint + | Ed25519PublicKey + | UTXOPublicKey + | ConditionType[] { + if (this[arg]) return this[arg]; + throw new Error(`Property ${arg} is not set in the Operation instance`); + } + + //========================================== + // Getter / Setter Methods + //========================================== + + /** + * Returns the UTXO operation type of this condition. + * + * @returns The operation type (Create, Deposit, or Withdraw) + * @throws {Error} If the operation is not set (should never happen with factory methods) + * + * @example + * ```typescript + * const condition = Condition.create(utxo, 1000n); + * console.log(condition.getOperation()); // "Create" + * ``` + */ + public getOperation(): UTXOOperationType { + return this.require("_op"); + } + + /** + * Returns the amount associated with this condition. + * + * @returns The amount in stroops as a bigint + * @throws {Error} If the amount is not set (should never happen with factory methods) + * + * @example + * ```typescript + * const condition = Condition.deposit(publicKey, 500n); + * console.log(condition.getAmount()); // 500n + * ``` + */ + public getAmount(): bigint { + return this.require("_amount"); + } + + /** + * Returns the conditions associated with this operation. + * + * @returns An array of Condition objects + * @throws {Error} If the conditions are not set + * + * @example + * ```typescript + * const operation = Operation.spend(utxo); + * const conditions = operation.getConditions(); + * console.log(conditions); // [Condition, Condition, ...] + * ``` + */ + public getConditions(): ConditionType[] { + return [...this.require("_conditions")]; + } + + /** + * Sets the conditions associated with this operation. + * @param conditions - An array of Condition objects to set + * @throws {Error} If the conditions are not set + */ + private setConditions(conditions: ConditionType[]) { + this._conditions = [...conditions]; + } + + /** + * Returns the Ed25519 public key for deposit or withdraw conditions. + * Only valid for DEPOSIT and WITHDRAW operations. + * + * @returns The Ed25519 public key in Stellar address format + * @throws {Error} If called on a CREATE condition or if public key is not set + * + * @example + * ```typescript + * const condition = Condition.deposit(publicKey, 500n); + * console.log(condition.getPublicKey()); // "GBXXXXXX..." + * ``` + */ + public getPublicKey(): Ed25519PublicKey { + return this.require("_publicKey"); + } + + /** + * Returns the UTXO public key for create conditions. + * Only valid for CREATE operations. + * + * @returns The UTXO public key as a Uint8Array + * @throws {Error} If called on a DEPOSIT or WITHDRAW condition or if UTXO is not set + * + * @example + * ```typescript + * const condition = Condition.create(utxo, 1000n); + * console.log(condition.getUtxo()); // Uint8Array(32) [...] + * ``` + */ + public getUtxo(): UTXOPublicKey { + return this.require("_utxo"); + } + + //========================================== + // Meta Management Methods + //========================================== + + public addCondition(condition: ConditionType): this { + const existingConditions = this.getConditions(); + this.setConditions([...existingConditions, condition]); + + return this; + } + + public addConditions(conditions: ConditionType[]): this { + const existingConditions = this.getConditions(); + this.setConditions([...existingConditions, ...conditions]); + + return this; + } + + public clearConditions(): this { + this.setConditions([]); + return this; + } + + //========================================== + // Type Guard Methods + //========================================== + + /** + * Type guard to check if this condition is a CREATE operation. + * Narrows the TypeScript type to CreateCondition when true. + * + * @returns True if this is a CREATE condition + * + * @example + * ```typescript + * const condition: Condition = getCondition(); + * if (condition.isCreate()) { + * // TypeScript knows this is CreateCondition + * const utxo = condition.getUtxo(); // Safe to call + * } + * ``` + */ + public isCreate(): this is CreateOperation { + return this.getOperation() === UTXOOperationType.CREATE; + } + + /** + * Type guard to check if this condition is a DEPOSIT operation. + * Narrows the TypeScript type to DepositCondition when true. + * + * @returns True if this is a DEPOSIT condition + * + * @example + * ```typescript + * const condition: Condition = getCondition(); + * if (condition.isDeposit()) { + * // TypeScript knows this is DepositCondition + * const key = condition.getPublicKey(); // Safe to call + * } + * ``` + */ + public isDeposit(): this is DepositOperation { + return this.getOperation() === UTXOOperationType.DEPOSIT; + } + + /** + * Type guard to check if this condition is a WITHDRAW operation. + * Narrows the TypeScript type to WithdrawCondition when true. + * + * @returns True if this is a WITHDRAW condition + * + * @example + * ```typescript + * const condition: Condition = getCondition(); + * if (condition.isWithdraw()) { + * // TypeScript knows this is WithdrawCondition + * const key = condition.getPublicKey(); // Safe to call + * } + * ``` + */ + public isWithdraw(): this is WithdrawOperation { + return this.getOperation() === UTXOOperationType.WITHDRAW; + } + + /** + * Type guard to check if this condition is a SPEND operation. + * Narrows the TypeScript type to SpendCondition when true. + * + * @returns True if this is a SPEND condition + * + * @example + * ```typescript + * const condition: Condition = getCondition(); + * if (condition.isSpend()) { + * // TypeScript knows this is SpendCondition + * const utxo = condition.getUtxo(); // Safe to call + * } + * ``` + */ + public isSpend(): this is SpendOperation { + return this.getOperation() === UTXOOperationType.SPEND; + } + + public hasConditions(): boolean { + return this._conditions !== undefined && + "length" in this._conditions && + this._conditions.length > 0 + ? true + : false; + } + + //========================================== + // Conversion Methods + //========================================== + + public toCondition(): ConditionType { + if (this.isCreate()) { + return Condition.create(this.getUtxo(), this.getAmount()); + } + + if (this.isDeposit()) { + return Condition.deposit(this.getPublicKey(), this.getAmount()); + } + + if (this.isWithdraw()) { + return Condition.withdraw(this.getPublicKey(), this.getAmount()); + } + + throw new Error("Cannot convert SPEND operation to Condition"); + } + + /** + * Converts this condition to Stellar's ScVal format for smart contract interaction. + * The ScVal format is used by Soroban smart contracts on the Stellar network. + * + * The resulting ScVal is a vector containing: + * - Operation symbol (Create/Deposit/Withdraw) + * - Address (UTXO bytes for CREATE, Stellar address for DEPOSIT/WITHDRAW) + * - Amount as i128 + * + * @returns The condition as a Stellar ScVal + * + * @example + * ```typescript + * const condition = Condition.deposit(publicKey, 500n); + * const scVal = condition.toScVal(); + * // Can now be used in Soroban contract invocations + * ``` + */ + public toScVal(): xdr.ScVal { + if (this.isCreate()) { + return this.createToScVal(); + } + + if (this.isDeposit()) { + return this.depositToScVal(); + } + + if (this.isWithdraw()) { + return this.withdrawToScVal(); + } + + if (this.isSpend()) { + return this.spendToScVal(); + } + + throw new Error("Unsupported operation type for ScVal conversion"); + } + + private conditionsToScVal(): xdr.ScVal { + return this.hasConditions() + ? xdr.ScVal.scvVec(this.getConditions().map((c) => c.toScVal())) + : xdr.ScVal.scvVec(null); + } + + private createToScVal(): xdr.ScVal { + if (!this.isCreate()) throw new Error("Operation is not CREATE"); + + return xdr.ScVal.scvVec([ + xdr.ScVal.scvBytes(Buffer.from(this.getUtxo())), + nativeToScVal(this.getAmount(), { type: "i128" }), + ]); + } + + private spendToScVal(): xdr.ScVal { + if (!this.isSpend()) throw new Error("Operation is not SPEND"); + return xdr.ScVal.scvVec([ + xdr.ScVal.scvBytes(Buffer.from(this.getUtxo())), + this.conditionsToScVal(), + ]); + } + + private depositToScVal(): xdr.ScVal { + if (!this.isDeposit()) throw new Error("Operation is not DEPOSIT"); + + return xdr.ScVal.scvVec([ + nativeToScVal(this.getPublicKey(), { type: "address" }), + nativeToScVal(this.getAmount(), { type: "i128" }), + this.conditionsToScVal(), + ]); + } + + private withdrawToScVal(): xdr.ScVal { + if (!this.isWithdraw()) throw new Error("Operation is not WITHDRAW"); + + return xdr.ScVal.scvVec([ + nativeToScVal(this.getPublicKey(), { type: "address" }), + nativeToScVal(this.getAmount(), { type: "i128" }), + this.conditionsToScVal(), + ]); + } + + /** + * Converts this condition to XDR (External Data Representation) format. + * XDR is the serialization format used by the Stellar network for all data structures. + * + * @returns The condition as a base64-encoded XDR string + * + * @example + * ```typescript + * const condition = Condition.create(utxo, 1000n); + * const xdr = condition.toXDR(); + * console.log(xdr); // "AAAABgAAAA..." (base64 string) + * // Can be transmitted over network or stored + * ``` + */ + public toXDR(): string { + return this.toScVal().toXDR("base64"); + } +} diff --git a/src/operation/types.ts b/src/operation/types.ts new file mode 100644 index 0000000..5587227 --- /dev/null +++ b/src/operation/types.ts @@ -0,0 +1,62 @@ +import type { Ed25519PublicKey } from "@colibri/core"; + +import type { xdr } from "@stellar/stellar-sdk"; +import type { + CreateCondition, + DepositCondition, + WithdrawCondition, +} from "../conditions/types.ts"; +import type { Condition } from "../conditions/types.ts"; +import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; + +export enum UTXOOperationType { + CREATE = "Create", + DEPOSIT = "Deposit", + WITHDRAW = "Withdraw", + SPEND = "Spend", +} + +export interface BaseOperation { + getOperation(): UTXOOperationType; + getAmount(): bigint; + isCreate(): this is CreateOperation; + isDeposit(): this is DepositOperation; + isWithdraw(): this is WithdrawOperation; + isSpend(): this is SpendOperation; + hasConditions(): boolean; + getConditions(): Condition[]; + addCondition(condition: Condition): this; + addConditions(condition: Condition[]): this; + clearConditions(): this; + toXDR(): string; + toScVal(): xdr.ScVal; +} + +export interface CreateOperation extends BaseOperation { + getOperation(): UTXOOperationType.CREATE; + getUtxo(): UTXOPublicKey; + toCondition(): CreateCondition; +} + +export interface DepositOperation extends BaseOperation { + getOperation(): UTXOOperationType.DEPOSIT; + getPublicKey(): Ed25519PublicKey; + toCondition(): DepositCondition; +} + +export interface WithdrawOperation extends BaseOperation { + getOperation(): UTXOOperationType.WITHDRAW; + getPublicKey(): Ed25519PublicKey; + toCondition(): WithdrawCondition; +} + +export interface SpendOperation extends BaseOperation { + getOperation(): UTXOOperationType.SPEND; + getUtxo(): UTXOPublicKey; +} + +export type MoonlightOperation = + | CreateOperation + | DepositOperation + | WithdrawOperation + | SpendOperation; diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index 1f77f96..2180102 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -12,7 +12,7 @@ import { ChannelSpec, } from "./constants.ts"; import type { ChannelInvoke, ChannelRead } from "./types.ts"; -import { xdr } from "@stellar/stellar-sdk"; +import type { xdr } from "@stellar/stellar-sdk"; export class PrivacyChannel { private _client: Contract; diff --git a/src/transaction-builder/index.ts b/src/transaction-builder/index.ts index e341388..2d7d0b1 100644 --- a/src/transaction-builder/index.ts +++ b/src/transaction-builder/index.ts @@ -6,26 +6,15 @@ import { xdr, } from "@stellar/stellar-sdk"; import { Buffer } from "buffer"; -import type { - CreateOperation, - DepositOperation, - SpendOperation, - WithdrawOperation, - UTXOPublicKey, - Ed25519PublicKey, -} from "../transaction-builder/types.ts"; -import type { StellarSmartContractId } from "../utils/types/stellar.types.ts"; + import { generateNonce } from "../utils/common/index.ts"; -import type { MoonlightOperation } from "../transaction-builder/types.ts"; import { buildAuthPayloadHash } from "../utils/auth/build-auth-payload.ts"; -import type { IUTXOKeypairBase } from "../core/utxo-keypair-base/types.ts"; -import { - createOpToXDR, - depositOpToXDR, - withdrawOpToXDR, - spendOpToXDR, -} from "./xdr/index.ts"; +import type { + IUTXOKeypairBase, + UTXOPublicKey, +} from "../core/utxo-keypair-base/types.ts"; + import { buildSignaturesXDR } from "./signatures/index.ts"; import { buildBundleAuthEntry, @@ -40,25 +29,36 @@ import { assertNoDuplicatePubKey, assertSpendExists, } from "./validators/index.ts"; -import type { Condition } from "../conditions/types.ts"; -import { type TransactionSigner, isTransactionSigner } from "@colibri/core"; +import { + type ContractId, + type Ed25519PublicKey, + type TransactionSigner, + isTransactionSigner, +} from "@colibri/core"; +import type { + CreateOperation, + DepositOperation, + SpendOperation, + WithdrawOperation, + MoonlightOperation, +} from "../operation/types.ts"; export class MoonlightTransactionBuilder { - private create: CreateOperation[] = []; - private spend: SpendOperation[] = []; - private deposit: DepositOperation[] = []; - private withdraw: WithdrawOperation[] = []; - private channelId: StellarSmartContractId; - private authId: StellarSmartContractId; - private asset: Asset; - private network: string; - private innerSignatures: Map = + private _create: CreateOperation[] = []; + private _spend: SpendOperation[] = []; + private _deposit: DepositOperation[] = []; + private _withdraw: WithdrawOperation[] = []; + private _channelId: ContractId; + private _authId: ContractId; + private _asset: Asset; + private _network: string; + private _innerSignatures: Map = new Map(); - private providerInnerSignatures: Map< + private _providerInnerSignatures: Map< Ed25519PublicKey, { sig: Buffer; exp: number; nonce: string } > = new Map(); - private extSignatures: Map = + private _extSignatures: Map = new Map(); constructor({ @@ -67,68 +67,295 @@ export class MoonlightTransactionBuilder { asset, network, }: { - channelId: StellarSmartContractId; - authId: StellarSmartContractId; + channelId: ContractId; + authId: ContractId; asset: Asset; network: string; }) { - this.channelId = channelId; - this.authId = authId; - this.asset = asset; - this.network = network; + this._channelId = channelId; + this._authId = authId; + this._asset = asset; + this._network = network; } - addCreate(utxo: UTXOPublicKey, amount: bigint) { - assertNoDuplicateCreate(this.create, utxo); - assertPositiveAmount(amount, "Create operation"); + //========================================== + // Meta Requirement Methods + //========================================== + + /** + * Internal helper method to safely retrieve required properties. + * Uses method overloading to provide type-safe access to private fields. + * + * @param arg - The name of the property to retrieve + * @returns The value of the requested property + * @throws {Error} If the requested property is not set + * @private + */ + private require(arg: "_create"): CreateOperation[]; + private require(arg: "_spend"): SpendOperation[]; + private require(arg: "_deposit"): DepositOperation[]; + private require(arg: "_withdraw"): WithdrawOperation[]; + private require(arg: "_channelId"): ContractId; + private require(arg: "_authId"): ContractId; + private require(arg: "_asset"): Asset; + private require(arg: "_network"): string; + private require( + arg: "_innerSignatures" + ): Map; + private require( + arg: "_providerInnerSignatures" + ): Map; + private require( + arg: "_extSignatures" + ): Map; + private require( + arg: + | "_create" + | "_spend" + | "_deposit" + | "_withdraw" + | "_channelId" + | "_authId" + | "_asset" + | "_network" + | "_innerSignatures" + | "_providerInnerSignatures" + | "_extSignatures" + ): + | CreateOperation[] + | SpendOperation[] + | DepositOperation[] + | WithdrawOperation[] + | ContractId + | Asset + | string + | Map + | Map + | Map { + if (this[arg]) return this[arg]; + throw new Error( + `Property ${arg} is not set in the Transaction Builder instance` + ); + } - this.create.push({ utxo, amount }); + //========================================== + // Getter / Setter Methods + //========================================== + + /** + * Returns the create operations in the transaction. + * + * @returns The create operations + * @throws {Error} If the create operations are not set + * + * @example + * ```typescript + * const condition = Condition.create(utxo, 1000n); + * console.log(condition.getOperation()); // "Create" + * ``` + */ + public getCreateOperations(): CreateOperation[] { + return this.require("_create"); + } + + private setCreateOperations(ops: CreateOperation[]) { + this._create = [...ops]; + } + + /** + * Returns the spend operations in the transaction. + * + * @returns The spend operations + * @throws {Error} If the spend operations are not set + * + * @example + * ```typescript + * const condition = Condition.spend(utxo, [condition1, condition2]); + * console.log(condition.getOperation()); // "Spend" + * ``` + */ + public getSpendOperations(): SpendOperation[] { + return this.require("_spend"); + } + + private setSpendOperations(ops: SpendOperation[]) { + this._spend = [...ops]; + } + + /** + * Returns the deposit operations in the transaction. + * + * @returns The deposit operations + * @throws {Error} If the deposit operations are not set + * + * @example + * ```typescript + * const condition = Condition.deposit(pubKey, 1000n, [condition1]); + * console.log(condition.getOperation()); // "Deposit" + * ``` + */ + public getDepositOperations(): DepositOperation[] { + return this.require("_deposit"); + } + + private setDepositOperations(ops: DepositOperation[]) { + this._deposit = [...ops]; + } + + /** + * Returns the withdraw operations in the transaction. + * + * @returns The withdraw operations + * @throws {Error} If the withdraw operations are not set + * + * @example + * ```typescript + * const condition = Condition.withdraw(pubKey, 1000n, [condition1]); + * console.log(condition.getOperation()); // "Withdraw" + * ``` + */ + public getWithdrawOperations(): WithdrawOperation[] { + return this.require("_withdraw"); + } + + private setWithdrawOperations(ops: WithdrawOperation[]) { + this._withdraw = [...ops]; + } + + /** + * Returns the channel ID associated with the transaction. + * + * @returns The channel ID + * @throws {Error} If the channel ID is not set + */ + public getChannelId(): ContractId { + return this.require("_channelId"); + } + + /** + * Returns the contract Id associated with the channel auth contract. + * + * @returns The auth ID + * @throws {Error} If the auth ID is not set + */ + public getAuthId(): ContractId { + return this.require("_authId"); + } + + /** + * Returns the asset associated with the transaction. + * + * @returns The asset + * @throws {Error} If the asset is not set + */ + public getAsset(): Asset { + return this.require("_asset"); + } + + /** + * Returns the network associated with the transaction. + * + * @returns The network + * @throws {Error} If the network is not set + */ + private get network(): string { + return this.require("_network"); + } + + /** + * Returns the inner signatures map. + * + * @returns The inner signatures map + * @throws {Error} If the inner signatures map is not set + */ + private get innerSignatures(): Map { + return this.require("_innerSignatures"); + } + + /** + * Returns the provider inner signatures map. + * + * @returns The provider inner signatures map + * @throws {Error} If the provider inner signatures map is not set + */ + private get providerInnerSignatures(): Map< + Ed25519PublicKey, + { sig: Buffer; exp: number; nonce: string } + > { + return this.require("_providerInnerSignatures"); + } + + /** + * Returns the external signatures map. + * + * @returns The external signatures map + * @throws {Error} If the external signatures map is not set + */ + private get extSignatures(): Map< + Ed25519PublicKey, + xdr.SorobanAuthorizationEntry + > { + return this.require("_extSignatures"); + } + + //========================================== + // - Methods + //========================================== + + addOperation(op: MoonlightOperation) { + if (op.isCreate()) return this.addCreate(op); + if (op.isSpend()) return this.addSpend(op); + if (op.isDeposit()) return this.addDeposit(op); + if (op.isWithdraw()) return this.addWithdraw(op); + throw new Error("Unsupported operation type"); + } + + private addCreate(op: CreateOperation) { + assertNoDuplicateCreate(this.getCreateOperations(), op); + assertPositiveAmount(op.getAmount(), "Create operation"); + + this.setCreateOperations([...this.getCreateOperations(), op]); return this; } - addSpend(utxo: UTXOPublicKey, conditions: Condition[]) { - assertNoDuplicateSpend(this.spend, utxo); + private addSpend(op: SpendOperation) { + assertNoDuplicateSpend(this.getSpendOperations(), op); - this.spend.push({ utxo, conditions }); + this.setSpendOperations([...this.getSpendOperations(), op]); return this; } - addDeposit( - pubKey: Ed25519PublicKey, - amount: bigint, - conditions: Condition[] - ) { - assertNoDuplicatePubKey(this.deposit, pubKey, "Deposit"); - assertPositiveAmount(amount, "Deposit operation"); + private addDeposit(op: DepositOperation) { + assertNoDuplicatePubKey(this.getDepositOperations(), op, "Deposit"); + assertPositiveAmount(op.getAmount(), "Deposit operation"); - this.deposit.push({ pubKey, amount, conditions }); + this.setDepositOperations([...this.getDepositOperations(), op]); return this; } - addWithdraw( - pubKey: Ed25519PublicKey, - amount: bigint, - conditions: Condition[] - ) { - assertNoDuplicatePubKey(this.withdraw, pubKey, "Withdraw"); - assertPositiveAmount(amount, "Withdraw operation"); + private addWithdraw(op: WithdrawOperation) { + assertNoDuplicatePubKey(this.getWithdrawOperations(), op, "Withdraw"); + assertPositiveAmount(op.getAmount(), "Withdraw operation"); - this.withdraw.push({ pubKey, amount, conditions }); + this.setWithdrawOperations([...this.getWithdrawOperations(), op]); return this; } - addInnerSignature( + public addInnerSignature( utxo: UTXOPublicKey, signature: Buffer, expirationLedger: number ) { - assertSpendExists(this.spend, utxo); + assertSpendExists(this.getSpendOperations(), utxo); - this.innerSignatures.set(utxo, { sig: signature, exp: expirationLedger }); + this.innerSignatures.set(utxo, { + sig: signature, + exp: expirationLedger, + }); return this; } - addProviderInnerSignature( + public addProviderInnerSignature( pubKey: Ed25519PublicKey, signature: Buffer, expirationLedger: number, @@ -142,13 +369,13 @@ export class MoonlightTransactionBuilder { return this; } - addExtSignedEntry( + public addExtSignedEntry( pubKey: Ed25519PublicKey, signedAuthEntry: xdr.SorobanAuthorizationEntry ) { if ( - !this.deposit.find((d) => d.pubKey === pubKey) && - !this.withdraw.find((d) => d.pubKey === pubKey) + !this.getDepositOperations().find((d) => d.getPublicKey() === pubKey) && + !this.getWithdrawOperations().find((d) => d.getPublicKey() === pubKey) ) throw new Error("No deposit or withdraw operation for this public key"); @@ -156,22 +383,15 @@ export class MoonlightTransactionBuilder { return this; } - getOperation(): MoonlightOperation { - return { - create: this.create, - spend: this.spend, - deposit: this.deposit, - withdraw: this.withdraw, - }; - } - - getDepositOperation( + public getDepositOperation( depositor: Ed25519PublicKey ): DepositOperation | undefined { - return this.deposit.find((d) => d.pubKey === depositor); + return this.getDepositOperations().find( + (d) => d.getPublicKey() === depositor + ); } - getExtAuthEntry( + public getExtAuthEntry( address: Ed25519PublicKey, nonce: string, signatureExpirationLedger: number @@ -180,33 +400,33 @@ export class MoonlightTransactionBuilder { if (!deposit) throw new Error("No deposit operation for this address"); return buildDepositAuthEntry({ - channelId: this.channelId, - assetId: this.asset.contractId(this.network), + channelId: this.getChannelId(), + assetId: this.getAsset().contractId(this.network), depositor: address, - amount: deposit.amount, + amount: deposit.getAmount(), conditions: [ - xdr.ScVal.scvVec(deposit.conditions.map((c) => c.toScVal())), + xdr.ScVal.scvVec(deposit.getConditions().map((c) => c.toScVal())), ], nonce, signatureExpirationLedger, }); } - getAuthRequirementArgs(): xdr.ScVal[] { - if (this.spend.length === 0) return []; + public getAuthRequirementArgs(): xdr.ScVal[] { + if (this.getSpendOperations().length === 0) return []; const signers: xdr.ScMapEntry[] = []; - const orderedSpend = orderSpendByUtxo(this.spend); + const orderedSpend = orderSpendByUtxo(this.getSpendOperations()); for (const spend of orderedSpend) { signers.push( new xdr.ScMapEntry({ key: xdr.ScVal.scvVec([ xdr.ScVal.scvSymbol("P256"), - xdr.ScVal.scvBytes(Buffer.from(spend.utxo as Uint8Array)), + xdr.ScVal.scvBytes(Buffer.from(spend.getUtxo() as Uint8Array)), ]), - val: xdr.ScVal.scvVec(spend.conditions.map((c) => c.toScVal())), + val: xdr.ScVal.scvVec(spend.getConditions().map((c) => c.toScVal())), }) ); } @@ -214,7 +434,7 @@ export class MoonlightTransactionBuilder { return [xdr.ScVal.scvVec([xdr.ScVal.scvMap(signers)])]; } - getOperationAuthEntry( + public getOperationAuthEntry( nonce: string, signatureExpirationLedger: number, signed: boolean = false @@ -222,8 +442,8 @@ export class MoonlightTransactionBuilder { const reqArgs: xdr.ScVal[] = this.getAuthRequirementArgs(); return buildBundleAuthEntry({ - channelId: this.channelId, - authId: this.authId, + channelId: this.getChannelId(), + authId: this.getAuthId(), args: reqArgs, nonce, signatureExpirationLedger, @@ -231,7 +451,7 @@ export class MoonlightTransactionBuilder { }); } - getSignedOperationAuthEntry(): xdr.SorobanAuthorizationEntry { + public getSignedOperationAuthEntry(): xdr.SorobanAuthorizationEntry { const providerSigners = Array.from(this.providerInnerSignatures.keys()); if (providerSigners.length === 0) @@ -243,8 +463,8 @@ export class MoonlightTransactionBuilder { const reqArgs: xdr.ScVal[] = this.getAuthRequirementArgs(); return buildBundleAuthEntry({ - channelId: this.channelId, - authId: this.authId, + channelId: this.getChannelId(), + authId: this.getAuthId(), args: reqArgs, nonce, signatureExpirationLedger, @@ -252,7 +472,7 @@ export class MoonlightTransactionBuilder { }); } - async getOperationAuthEntryHash( + public async getOperationAuthEntryHash( nonce: string, signatureExpirationLedger: number ): Promise { @@ -268,7 +488,7 @@ export class MoonlightTransactionBuilder { }); } - signaturesXDR(): string { + public signaturesXDR(): string { const providerSigners = Array.from(this.providerInnerSignatures.keys()); if (providerSigners.length === 0) throw new Error("No Provider signatures added"); @@ -284,7 +504,7 @@ export class MoonlightTransactionBuilder { return buildSignaturesXDR(spendSigs, providerSigs); } - async signWithProvider( + public async signWithProvider( providerKeys: TransactionSigner | Keypair, signatureExpirationLedger: number, nonce?: string @@ -309,18 +529,19 @@ export class MoonlightTransactionBuilder { ); } - async signWithSpendUtxo( + public async signWithSpendUtxo( utxo: IUTXOKeypairBase, signatureExpirationLedger: number ) { - const conditions = this.spend.find((s) => - Buffer.from(s.utxo).equals(Buffer.from(utxo.publicKey)) - )?.conditions; + const conditions = this.getSpendOperations() + .find((s) => Buffer.from(s.getUtxo()).equals(Buffer.from(utxo.publicKey))) + ?.getConditions(); + if (!conditions) throw new Error("No spend operation for this UTXO"); const signedHash = await utxo.signPayload( await buildAuthPayloadHash({ - contractId: this.channelId, + contractId: this.getChannelId(), conditions, liveUntilLedger: signatureExpirationLedger, }) @@ -333,7 +554,7 @@ export class MoonlightTransactionBuilder { ); } - async signExtWithEd25519( + public async signExtWithEd25519( keys: TransactionSigner | Keypair, signatureExpirationLedger: number, nonce?: string @@ -368,7 +589,7 @@ export class MoonlightTransactionBuilder { ); } - getSignedAuthEntries(): xdr.SorobanAuthorizationEntry[] { + public getSignedAuthEntries(): xdr.SorobanAuthorizationEntry[] { const signedEntries = [ ...Array.from(this.extSignatures.values()), this.getSignedOperationAuthEntry(), @@ -376,9 +597,9 @@ export class MoonlightTransactionBuilder { return signedEntries; } - getInvokeOperation(): xdr.Operation { + public getInvokeOperation(): xdr.Operation { return Operation.invokeContractFunction({ - contract: this.channelId, + contract: this.getChannelId(), function: "transact", @@ -387,23 +608,31 @@ export class MoonlightTransactionBuilder { }); } - buildXDR(): xdr.ScVal { + public buildXDR(): xdr.ScVal { return xdr.ScVal.scvMap([ new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol("create"), - val: xdr.ScVal.scvVec(this.create.map((op) => createOpToXDR(op))), + val: xdr.ScVal.scvVec( + this.getCreateOperations().map((op) => op.toScVal()) + ), }), new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol("deposit"), - val: xdr.ScVal.scvVec(this.deposit.map((op) => depositOpToXDR(op))), + val: xdr.ScVal.scvVec( + this.getDepositOperations().map((op) => op.toScVal()) + ), }), new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol("spend"), - val: xdr.ScVal.scvVec(this.spend.map((op) => spendOpToXDR(op))), + val: xdr.ScVal.scvVec( + this.getSpendOperations().map((op) => op.toScVal()) + ), }), new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol("withdraw"), - val: xdr.ScVal.scvVec(this.withdraw.map((op) => withdrawOpToXDR(op))), + val: xdr.ScVal.scvVec( + this.getWithdrawOperations().map((op) => op.toScVal()) + ), }), ]); } diff --git a/src/transaction-builder/index.unit.test.ts b/src/transaction-builder/index.unit.test.ts index b4f4b76..e0f9008 100644 --- a/src/transaction-builder/index.unit.test.ts +++ b/src/transaction-builder/index.unit.test.ts @@ -1,1566 +1,848 @@ -// deno-lint-ignore-file require-await -import { - assertEquals, - assertThrows, -} from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { assertEquals, assertExists, assertThrows } from "@std/assert"; +import { beforeAll, describe, it } from "@std/testing/bdd"; +import { LocalSigner } from "@colibri/core"; +import { Asset, Networks } from "@stellar/stellar-sdk"; +import type { Ed25519PublicKey, ContractId } from "@colibri/core"; import { MoonlightTransactionBuilder } from "./index.ts"; -import { - createOpToXDR, - depositOpToXDR, - withdrawOpToXDR, - spendOpToXDR, -} from "./xdr/index.ts"; -import { Asset, Keypair, StrKey, xdr } from "@stellar/stellar-sdk"; -import { Buffer } from "buffer"; import { Condition } from "../conditions/index.ts"; -import { StellarSmartContractId } from "../utils/types/stellar.types.ts"; - -// Mock data for testing -const mockChannelId: StellarSmartContractId = StrKey.encodeContract( - Buffer.alloc(32) -) as StellarSmartContractId; -const mockAuthId: StellarSmartContractId = StrKey.encodeContract( - Buffer.alloc(32, 1) -) as StellarSmartContractId; -const mockNetwork = "testnet"; -const mockAsset = Asset.native(); - -// Mock UTXO data -const mockUTXO1 = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); -const mockUTXO2 = Buffer.from([9, 10, 11, 12, 13, 14, 15, 16]); - -// Mock Ed25519 public keys -const mockEd25519Key1 = Keypair.random().publicKey() as `G${string}`; -const mockEd25519Key2 = Keypair.random().publicKey() as `G${string}`; - -// Mock conditions -const mockCreateCondition = Condition.create(mockUTXO1, 1000n); - -const mockDepositCondition = Condition.deposit(mockEd25519Key1, 500n); - -const mockWithdrawCondition = Condition.withdraw(mockEd25519Key2, 300n); - -// Helper function to create a test builder instance -function createTestBuilder(): MoonlightTransactionBuilder { - return new MoonlightTransactionBuilder({ - channelId: mockChannelId, - authId: mockAuthId, - asset: mockAsset, - network: mockNetwork, - }); -} +import { MoonlightOperation as Operation } from "../operation/index.ts"; -Deno.test( - "MoonlightTransactionBuilder - Basic Operations (Add Methods)", - async (t) => { - await t.step( - "addCreate should add create operation with valid parameters", - () => { - const builder = createTestBuilder(); +import { generateP256KeyPair } from "../utils/secp256r1/generateP256KeyPair.ts"; +import { Buffer } from "buffer"; +import { generateNonce } from "../utils/common/index.ts"; +import { UTXOKeypairBase } from "../core/utxo-keypair-base/index.ts"; +import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; + +describe("MoonlightTransactionBuilder", () => { + let validPublicKey: Ed25519PublicKey; + + let validAmount: bigint; + let channelId: ContractId; + let authId: ContractId; + let asset: Asset; + let network: string; + let builder: MoonlightTransactionBuilder; + + beforeAll(() => { + validPublicKey = + LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + + validAmount = 1000n; + channelId = + "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC" as ContractId; + authId = + "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA" as ContractId; + asset = Asset.native(); + network = Networks.TESTNET; + + builder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); + }); - const result = builder.addCreate(mockUTXO1, 1000n); + describe("Construction", () => { + it("should create a transaction builder with valid parameters", () => { + const txBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); + + assertExists(txBuilder); + assertEquals(txBuilder.getChannelId(), channelId); + assertEquals(txBuilder.getAuthId(), authId); + assertEquals(txBuilder.getAsset(), asset); + }); - // Should return builder instance for chaining - assertEquals(result, builder); + it("should initialize with empty operation arrays", () => { + const txBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); + + assertEquals(txBuilder.getCreateOperations().length, 0); + assertEquals(txBuilder.getSpendOperations().length, 0); + assertEquals(txBuilder.getDepositOperations().length, 0); + assertEquals(txBuilder.getWithdrawOperations().length, 0); + }); + }); - // Should have added the create operation - const operations = builder.getOperation(); - assertEquals(operations.create.length, 1); - assertEquals(operations.create[0].utxo, mockUTXO1); - assertEquals(operations.create[0].amount, 1000n); - } - ); + describe("Features", () => { + describe("Operation Management", () => { + it("should add CREATE operations successfully", async () => { + const utxo = (await generateP256KeyPair()).publicKey as UTXOPublicKey; + const operation = Operation.create(utxo, validAmount); + // Note: CREATE operations don't have conditions + + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); + + testBuilder.addOperation(operation); + + const creates = testBuilder.getCreateOperations(); + assertEquals(creates.length, 1); + assertEquals(creates[0].getAmount(), validAmount); + }); + + it("should return correct deposit operation by public key", () => { + const pubKey = + LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + const condition = Condition.deposit(pubKey, validAmount); + const operation = Operation.deposit(pubKey, validAmount); + operation.addCondition(condition); + + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); + + // deno-lint-ignore no-explicit-any + (testBuilder as any).addDeposit(operation); + + const foundOperation = testBuilder.getDepositOperation(pubKey); + assertExists(foundOperation); + assertEquals(foundOperation.getPublicKey(), pubKey); + assertEquals(foundOperation.getAmount(), validAmount); + }); + + it("should return undefined for non-existent deposit operation", () => { + const nonExistentKey = + LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + const foundOperation = builder.getDepositOperation(nonExistentKey); + assertEquals(foundOperation, undefined); + }); + }); - await t.step( - "addCreate should throw error when UTXO already exists", - () => { - const builder = createTestBuilder(); + describe("Signature Management", () => { + it("should add inner signatures for UTXO operations", async () => { + const utxo = (await generateP256KeyPair()).publicKey as UTXOPublicKey; + const condition = Condition.create(utxo, validAmount); + const spendOperation = Operation.spend(utxo); + spendOperation.addCondition(condition); - builder.addCreate(mockUTXO1, 1000n); + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); - // Should throw error when adding same UTXO again - assertThrows( - () => builder.addCreate(mockUTXO1, 2000n), - Error, - "Create operation for this UTXO already exists" - ); - } - ); + // Add spend operation first + // deno-lint-ignore no-explicit-any + (testBuilder as any).addSpend(spendOperation); - await t.step( - "addCreate should throw error when amount is zero or negative", - () => { - const builder = createTestBuilder(); + const signature = Buffer.from("test_signature"); + const expirationLedger = 1000000; - // Should throw error for zero amount - assertThrows( - () => builder.addCreate(mockUTXO1, 0n), - Error, - "Create operation amount must be positive" - ); + testBuilder.addInnerSignature(utxo, signature, expirationLedger); - // Should throw error for negative amount - assertThrows( - () => builder.addCreate(mockUTXO2, -100n), - Error, - "Create operation amount must be positive" - ); - } - ); - - await t.step( - "addSpend should add spend operation with valid parameters", - () => { - const builder = createTestBuilder(); - const conditions = [mockCreateCondition, mockDepositCondition]; - - const result = builder.addSpend(mockUTXO1, conditions); - - // Should return builder instance for chaining - assertEquals(result, builder); - - // Should have added the spend operation - const operations = builder.getOperation(); - assertEquals(operations.spend.length, 1); - assertEquals(operations.spend[0].utxo, mockUTXO1); - assertEquals(operations.spend[0].conditions.length, 2); - assertEquals(operations.spend[0].conditions[0], mockCreateCondition); - assertEquals(operations.spend[0].conditions[1], mockDepositCondition); - } - ); - - await t.step("addSpend should throw error when UTXO already exists", () => { - const builder = createTestBuilder(); - const conditions = [mockCreateCondition]; - - builder.addSpend(mockUTXO1, conditions); - - // Should throw error when adding same UTXO again - assertThrows( - () => builder.addSpend(mockUTXO1, [mockWithdrawCondition]), - Error, - "Spend operation for this UTXO already exists" - ); - }); + // deno-lint-ignore no-explicit-any + const signatures = (testBuilder as any).innerSignatures; + assertEquals(signatures.size, 1); + }); - await t.step("addSpend should handle empty conditions array", () => { - const builder = createTestBuilder(); + it("should add provider inner signatures", () => { + const signature = Buffer.from("provider_signature"); + const expirationLedger = 1000000; + const nonce = generateNonce(); - const result = builder.addSpend(mockUTXO1, []); + builder.addProviderInnerSignature( + validPublicKey, + signature, + expirationLedger, + nonce + ); - // Should return builder instance for chaining - assertEquals(result, builder); + // deno-lint-ignore no-explicit-any + const providerSigs = (builder as any).providerInnerSignatures; + assertEquals(providerSigs.size >= 1, true); + }); + it("should sign with provider using keypair", async () => { + const providerKeys = LocalSigner.generateRandom(); + const signatureExpirationLedger = 1000000; + const nonce = generateNonce(); + + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); + + await testBuilder.signWithProvider( + providerKeys, + signatureExpirationLedger, + nonce + ); - // Should have added the spend operation with empty conditions - const operations = builder.getOperation(); - assertEquals(operations.spend.length, 1); - assertEquals(operations.spend[0].utxo, mockUTXO1); - assertEquals(operations.spend[0].conditions.length, 0); - }); + // deno-lint-ignore no-explicit-any + const providerSigs = (testBuilder as any).providerInnerSignatures; + assertEquals(providerSigs.size, 1); + assertEquals( + providerSigs.has(providerKeys.publicKey() as Ed25519PublicKey), + true + ); + }); + + it("should sign with provider and auto-generate nonce if not provided", async () => { + const providerKeys = LocalSigner.generateRandom(); + const signatureExpirationLedger = 1000000; + + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); + + await testBuilder.signWithProvider( + providerKeys, + signatureExpirationLedger + ); - await t.step( - "addDeposit should add deposit operation with valid parameters", - () => { - const builder = createTestBuilder(); - const conditions = [mockDepositCondition]; + // deno-lint-ignore no-explicit-any + const providerSigs = (testBuilder as any).providerInnerSignatures; + assertEquals(providerSigs.size, 1); - const result = builder.addDeposit(mockEd25519Key1, 500n, conditions); + // Verify nonce was auto-generated + const sigData = providerSigs.get( + providerKeys.publicKey() as Ed25519PublicKey + ); + assertExists(sigData.nonce); + }); - // Should return builder instance for chaining - assertEquals(result, builder); + it("should sign with spend UTXO", async () => { + const utxoKeys = new UTXOKeypairBase(await generateP256KeyPair()); + const utxo = utxoKeys.publicKey as UTXOPublicKey; + const spendOperation = Operation.spend(utxo); - // Should have added the deposit operation - const operations = builder.getOperation(); - assertEquals(operations.deposit.length, 1); - assertEquals(operations.deposit[0].pubKey, mockEd25519Key1); - assertEquals(operations.deposit[0].amount, 500n); - assertEquals(operations.deposit[0].conditions.length, 1); - assertEquals(operations.deposit[0].conditions[0], mockDepositCondition); - } - ); + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); - await t.step( - "addDeposit should throw error when public key already exists", - () => { - const builder = createTestBuilder(); - const conditions = [mockDepositCondition]; + // deno-lint-ignore no-explicit-any + (testBuilder as any).addSpend(spendOperation); - builder.addDeposit(mockEd25519Key1, 500n, conditions); + const signatureExpirationLedger = 1000000; - // Should throw error when adding same public key again - assertThrows( - () => builder.addDeposit(mockEd25519Key1, 1000n, []), - Error, - "Deposit operation for this public key already exists" + await testBuilder.signWithSpendUtxo( + utxoKeys, + signatureExpirationLedger ); - } - ); - await t.step( - "addDeposit should throw error when amount is zero or negative", - () => { - const builder = createTestBuilder(); - const conditions = [mockDepositCondition]; + // deno-lint-ignore no-explicit-any + const innerSigs = (testBuilder as any).innerSignatures; + assertEquals(innerSigs.size, 1); + }); - // Should throw error for zero amount - assertThrows( - () => builder.addDeposit(mockEd25519Key1, 0n, conditions), - Error, - "Deposit operation amount must be positive" - ); + it("should sign external entry with Ed25519 keys", async () => { + const userKeys = LocalSigner.generateRandom(); + const pubKey = userKeys.publicKey() as Ed25519PublicKey; + const condition = Condition.deposit(pubKey, validAmount); + const operation = Operation.deposit(pubKey, validAmount); + operation.addCondition(condition); - // Should throw error for negative amount - assertThrows( - () => builder.addDeposit(mockEd25519Key2, -100n, conditions), - Error, - "Deposit operation amount must be positive" - ); - } - ); - - await t.step( - "addWithdraw should add withdraw operation with valid parameters", - () => { - const builder = createTestBuilder(); - const conditions = [mockWithdrawCondition]; - - const result = builder.addWithdraw(mockEd25519Key1, 300n, conditions); - - // Should return builder instance for chaining - assertEquals(result, builder); - - // Should have added the withdraw operation - const operations = builder.getOperation(); - assertEquals(operations.withdraw.length, 1); - assertEquals(operations.withdraw[0].pubKey, mockEd25519Key1); - assertEquals(operations.withdraw[0].amount, 300n); - assertEquals(operations.withdraw[0].conditions.length, 1); - assertEquals( - operations.withdraw[0].conditions[0], - mockWithdrawCondition - ); - } - ); - - await t.step( - "addWithdraw should throw error when public key already exists", - () => { - const builder = createTestBuilder(); - const conditions = [mockWithdrawCondition]; + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); - builder.addWithdraw(mockEd25519Key1, 300n, conditions); + // deno-lint-ignore no-explicit-any + (testBuilder as any).addDeposit(operation); - // Should throw error when adding same public key again - assertThrows( - () => builder.addWithdraw(mockEd25519Key1, 500n, []), - Error, - "Withdraw operation for this public key already exists" - ); - } - ); + const nonce = generateNonce(); + const signatureExpirationLedger = 1000000; - await t.step( - "addWithdraw should throw error when amount is zero or negative", - () => { - const builder = createTestBuilder(); - const conditions = [mockWithdrawCondition]; + await testBuilder.signExtWithEd25519( + userKeys, - // Should throw error for zero amount - assertThrows( - () => builder.addWithdraw(mockEd25519Key1, 0n, conditions), - Error, - "Withdraw operation amount must be positive" + signatureExpirationLedger, + nonce ); - // Should throw error for negative amount - assertThrows( - () => builder.addWithdraw(mockEd25519Key2, -100n, conditions), - Error, - "Withdraw operation amount must be positive" + // deno-lint-ignore no-explicit-any + const extSigs = (testBuilder as any).extSignatures; + assertEquals(extSigs.size, 1); + assertEquals(extSigs.has(pubKey), true); + }); + + it("should add external signed entry", () => { + const pubKey = + LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + const condition = Condition.deposit(pubKey, validAmount); + const operation = Operation.deposit(pubKey, validAmount); + operation.addCondition(condition); + + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); + + // deno-lint-ignore no-explicit-any + (testBuilder as any).addDeposit(operation); + + const nonce = generateNonce(); + const signatureExpirationLedger = 1000000; + const authEntry = testBuilder.getExtAuthEntry( + pubKey, + nonce, + signatureExpirationLedger ); - } - ); - - await t.step("should allow chaining multiple operations", () => { - const builder = createTestBuilder(); - - const result = builder - .addCreate(mockUTXO1, 1000n) - .addCreate(mockUTXO2, 2000n) - .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]) - .addWithdraw(mockEd25519Key2, 300n, [mockWithdrawCondition]); - - // Should return builder instance - assertEquals(result, builder); - - // Should have all operations - const operations = builder.getOperation(); - assertEquals(operations.create.length, 2); - assertEquals(operations.spend.length, 1); - assertEquals(operations.deposit.length, 1); - assertEquals(operations.withdraw.length, 1); - }); - } -); - -Deno.test("MoonlightTransactionBuilder - Internal Signatures", async (t) => { - await t.step( - "addInnerSignature should add signature for existing spend operation", - () => { - const builder = createTestBuilder(); - const mockSignature = Buffer.alloc(64, 0x42); - const expirationLedger = 1000; - - // First add a spend operation - builder.addSpend(mockUTXO1, [mockCreateCondition]); - - const result = builder.addInnerSignature( - mockUTXO1, - mockSignature, - expirationLedger - ); - - // Should return builder instance for chaining - assertEquals(result, builder); - - // Verify signature was added (we can't directly access private properties, - // but we can test that the method doesn't throw and returns the builder) - assertEquals(result instanceof MoonlightTransactionBuilder, true); - } - ); - - await t.step( - "addInnerSignature should throw error when UTXO not found in spend operations", - () => { - const builder = createTestBuilder(); - const mockSignature = Buffer.alloc(64, 0x42); - const expirationLedger = 1000; - - // Don't add any spend operations - - // Should throw error when trying to add signature for non-existent UTXO - assertThrows( - () => - builder.addInnerSignature(mockUTXO1, mockSignature, expirationLedger), - Error, - "No spend operation for this UTXO" - ); - } - ); - - await t.step( - "addProviderInnerSignature should add provider signature", - () => { - const builder = createTestBuilder(); - const mockSignature = Buffer.alloc(64, 0x43); - const expirationLedger = 1000; - const nonce = "123456789"; - - const result = builder.addProviderInnerSignature( - mockEd25519Key1, - mockSignature, - expirationLedger, - nonce - ); - - // Should return builder instance for chaining - assertEquals(result, builder); - - // Verify the method doesn't throw and returns the builder - assertEquals(result instanceof MoonlightTransactionBuilder, true); - } - ); - - await t.step( - "addExtSignedEntry should add external signature for existing deposit", - () => { - const builder = createTestBuilder(); - const mockAuthEntry = {} as xdr.SorobanAuthorizationEntry; - - // First add a deposit operation - builder.addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]); - - const result = builder.addExtSignedEntry(mockEd25519Key1, mockAuthEntry); - - // Should return builder instance for chaining - assertEquals(result, builder); - - // Verify the method doesn't throw and returns the builder - assertEquals(result instanceof MoonlightTransactionBuilder, true); - } - ); - - await t.step( - "addExtSignedEntry should add external signature for existing withdraw", - () => { - const builder = createTestBuilder(); - const mockAuthEntry = {} as xdr.SorobanAuthorizationEntry; - - // First add a withdraw operation - builder.addWithdraw(mockEd25519Key1, 300n, [mockWithdrawCondition]); - - const result = builder.addExtSignedEntry(mockEd25519Key1, mockAuthEntry); - - // Should return builder instance for chaining - assertEquals(result, builder); - - // Verify the method doesn't throw and returns the builder - assertEquals(result instanceof MoonlightTransactionBuilder, true); - } - ); - - await t.step( - "addExtSignedEntry should throw error when public key not found", - () => { - const builder = createTestBuilder(); - const mockAuthEntry = {} as xdr.SorobanAuthorizationEntry; - - // Don't add any deposit or withdraw operations - - // Should throw error when trying to add signature for non-existent public key - assertThrows( - () => builder.addExtSignedEntry(mockEd25519Key1, mockAuthEntry), - Error, - "No deposit or withdraw operation for this public key" - ); - } - ); - - await t.step("should allow chaining signature operations", () => { - const builder = createTestBuilder(); - const mockSignature = Buffer.alloc(64, 0x44); - const mockAuthEntry = {} as xdr.SorobanAuthorizationEntry; - - // Add operations first - builder.addSpend(mockUTXO1, [mockCreateCondition]); - builder.addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]); - - const result = builder - .addInnerSignature(mockUTXO1, mockSignature, 1000) - .addProviderInnerSignature( - mockEd25519Key1, - mockSignature, - 1000, - "nonce123" - ) - .addExtSignedEntry(mockEd25519Key1, mockAuthEntry); - - // Should return builder instance - assertEquals(result, builder); - - // Verify the method doesn't throw and returns the builder - assertEquals(result instanceof MoonlightTransactionBuilder, true); - }); - await t.step("should handle multiple provider signatures", () => { - const builder = createTestBuilder(); - const mockSignature1 = Buffer.alloc(64, 0x45); - const mockSignature2 = Buffer.alloc(64, 0x46); - - const result = builder - .addProviderInnerSignature( - mockEd25519Key1, - mockSignature1, - 1000, - "nonce1" - ) - .addProviderInnerSignature( - mockEd25519Key2, - mockSignature2, - 1000, - "nonce2" - ); - - // Should return builder instance - assertEquals(result, builder); - - // Verify the method doesn't throw and returns the builder - assertEquals(result instanceof MoonlightTransactionBuilder, true); - }); + testBuilder.addExtSignedEntry(pubKey, authEntry); - await t.step( - "should handle multiple inner signatures for different UTXOs", - () => { - const builder = createTestBuilder(); - const mockSignature1 = Buffer.alloc(64, 0x47); - const mockSignature2 = Buffer.alloc(64, 0x48); + // deno-lint-ignore no-explicit-any + const extSigs = (testBuilder as any).extSignatures; + assertEquals(extSigs.size, 1); + assertEquals(extSigs.has(pubKey), true); + }); + }); - // Add spend operations for different UTXOs - builder.addSpend(mockUTXO1, [mockCreateCondition]); - builder.addSpend(mockUTXO2, [mockDepositCondition]); + describe("XDR Generation", () => { + it("should build XDR with empty operations", () => { + const xdr = builder.buildXDR(); - const result = builder - .addInnerSignature(mockUTXO1, mockSignature1, 1000) - .addInnerSignature(mockUTXO2, mockSignature2, 1000); + assertExists(xdr); + assertEquals(xdr.switch().name, "scvMap"); - // Should return builder instance - assertEquals(result, builder); + const mapEntries = xdr.map(); + if (mapEntries) { + assertEquals(mapEntries.length, 4); // create, deposit, spend, withdraw + } + }); + + it("should build XDR with operations", async () => { + const utxo = (await generateP256KeyPair()).publicKey as UTXOPublicKey; + const operation = Operation.create(utxo, validAmount); + // Note: CREATE operations don't have conditions + + const builderWithOps = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); + + builderWithOps.addOperation(operation); + const xdr = builderWithOps.buildXDR(); + + assertExists(xdr); + assertEquals(xdr.switch().name, "scvMap"); + }); + }); - // Verify the method doesn't throw and returns the builder - assertEquals(result instanceof MoonlightTransactionBuilder, true); - } - ); -}); + describe("Auth Entry Generation", () => { + it("should generate operation auth entry with valid parameters", () => { + const nonce = generateNonce(); + const signatureExpirationLedger = 1000000; -Deno.test("MoonlightTransactionBuilder - Query Methods", async (t) => { - await t.step( - "getOperation should return empty arrays when no operations added", - () => { - const builder = createTestBuilder(); - - const op = builder.getOperation(); - assertEquals(op.create.length, 0); - assertEquals(op.spend.length, 0); - assertEquals(op.deposit.length, 0); - assertEquals(op.withdraw.length, 0); - } - ); - - await t.step("getOperation should reflect added operations", () => { - const builder = createTestBuilder(); - - builder - .addCreate(mockUTXO1, 1000n) - .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]) - .addWithdraw(mockEd25519Key2, 300n, [mockWithdrawCondition]); - - const op = builder.getOperation(); - assertEquals(op.create.length, 1); - assertEquals(op.spend.length, 1); - assertEquals(op.deposit.length, 1); - assertEquals(op.withdraw.length, 1); - }); + const authEntry = builder.getOperationAuthEntry( + nonce, + signatureExpirationLedger + ); - await t.step("getDepositOperation should return deposit when exists", () => { - const builder = createTestBuilder(); - builder.addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]); + assertExists(authEntry); + assertEquals(typeof authEntry.toXDR, "function"); + }); - const dep = builder.getDepositOperation(mockEd25519Key1); - assertEquals(dep?.pubKey, mockEd25519Key1); - assertEquals(dep?.amount, 500n); - assertEquals(dep?.conditions.length, 1); - }); + it("should generate operation auth entry hash", async () => { + const nonce = generateNonce(); + const signatureExpirationLedger = 1000000; - await t.step( - "getDepositOperation should return undefined when not found", - () => { - const builder = createTestBuilder(); - const dep = builder.getDepositOperation(mockEd25519Key2); - assertEquals(dep, undefined); - } - ); -}); + const hash = await builder.getOperationAuthEntryHash( + nonce, + signatureExpirationLedger + ); -Deno.test( - "MoonlightTransactionBuilder - Authorization and Arguments", - async (t) => { - await t.step( - "getExtAuthEntry should generate entry for existing deposit", - () => { - const builder = createTestBuilder(); - builder.addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]); - - // Using deterministic values for validation - const nonce = "123"; - const exp = 456; - - const entry = builder.getExtAuthEntry(mockEd25519Key1, nonce, exp); - // We can't assert XDR internals without full mocks; ensure object exists - assertEquals(!!entry, true); - } - ); - - await t.step("getExtAuthEntry should throw when deposit is missing", () => { - const builder = createTestBuilder(); - const nonce = "123"; - const exp = 456; - - assertThrows( - () => builder.getExtAuthEntry(mockEd25519Key1, nonce, exp), - Error, - "No deposit operation for this address" - ); - }); + assertExists(hash); + assertEquals(Buffer.isBuffer(hash), true); + assertEquals(hash.length > 0, true); + }); + + it("should generate external auth entry for deposit operation", () => { + const pubKey = + LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + const condition = Condition.deposit(pubKey, validAmount); + const operation = Operation.deposit(pubKey, validAmount); + operation.addCondition(condition); + + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); + + // Add deposit operation + // deno-lint-ignore no-explicit-any + (testBuilder as any).addDeposit(operation); + + const nonce = generateNonce(); + const signatureExpirationLedger = 1000000; + + const authEntry = testBuilder.getExtAuthEntry( + pubKey, + nonce, + signatureExpirationLedger + ); - await t.step( - "getAuthRequirementArgs should return empty when no spend", - () => { - const builder = createTestBuilder(); - const args = builder.getAuthRequirementArgs(); - assertEquals(Array.isArray(args), true); - assertEquals(args.length, 0); - } - ); - - await t.step( - "getAuthRequirementArgs should include ordered spend signers", - () => { - const builder = createTestBuilder(); - // Add spend with two UTXOs in reverse order to verify ordering - builder - .addSpend(mockUTXO2, [mockDepositCondition]) - .addSpend(mockUTXO1, [mockCreateCondition]); - - const args = builder.getAuthRequirementArgs(); - // Expect one vector with one map of signers - assertEquals(args.length, 1); - // We can't deserialize xdr.ScVal here; presence suffices for unit test - assertEquals(!!args[0], true); - } - ); - - await t.step( - "getOperationAuthEntry should generate entry (unsigned)", - () => { - const builder = createTestBuilder(); - // No spend: args should be empty, but entry is still generated - const entry = builder.getOperationAuthEntry("999", 1234, false); - assertEquals(!!entry, true); - } - ); - } -); - -Deno.test("MoonlightTransactionBuilder - Hash and Signature XDR", async (t) => { - await t.step( - "getOperationAuthEntryHash should return hash for given parameters", - async () => { - const builder = createTestBuilder(); - const nonce = "123456789"; - const exp = 1000; - - const hash = await builder.getOperationAuthEntryHash(nonce, exp); - // Should return a 32-byte hash - assertEquals(hash.length, 32); - assertEquals(hash instanceof Uint8Array, true); - } - ); - - await t.step( - "getOperationAuthEntryHash should use network ID correctly", - async () => { - const builder = createTestBuilder(); - const nonce = "123456789"; - const exp = 1000; - - const hash1 = await builder.getOperationAuthEntryHash(nonce, exp); - const hash2 = await builder.getOperationAuthEntryHash(nonce, exp); - // Same parameters should produce same hash - assertEquals(hash1, hash2); - } - ); - - await t.step( - "getOperationAuthEntryHash should handle different nonce values", - async () => { - const builder = createTestBuilder(); - const exp = 1000; - - const hash1 = await builder.getOperationAuthEntryHash("123456789", exp); - const hash2 = await builder.getOperationAuthEntryHash("987654321", exp); - // Different nonces should produce different hashes - assertEquals(hash1.length, 32); - assertEquals(hash2.length, 32); - // Hashes should be different - assertEquals( - hash1.every((byte, i) => byte === hash2[i]), - false - ); - } - ); - - await t.step( - "signaturesXDR should throw error when no provider signatures", - () => { - const builder = createTestBuilder(); - // Add spend operation but no provider signature - builder.addSpend(mockUTXO1, [mockCreateCondition]); - - assertThrows( - () => builder.signaturesXDR(), - Error, - "No Provider signatures added" - ); - } - ); - - await t.step("signaturesXDR should return correct XDR format", () => { - const builder = createTestBuilder(); - const mockSignature = Buffer.alloc(64, 0x42); - - // Add provider signature - builder.addProviderInnerSignature( - mockEd25519Key1, - mockSignature, - 1000, - "nonce123" - ); - - const xdrString = builder.signaturesXDR(); - // Should return a base64 XDR string - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - }); + assertExists(authEntry); + assertEquals(typeof authEntry.toXDR, "function"); + }); - await t.step("signaturesXDR should order signatures correctly", () => { - const builder = createTestBuilder(); - const mockSignature1 = Buffer.alloc(64, 0x42); - const mockSignature2 = Buffer.alloc(64, 0x43); - - // Add provider signatures in reverse order - builder - .addProviderInnerSignature( - mockEd25519Key2, - mockSignature2, - 1000, - "nonce2" - ) - .addProviderInnerSignature( - mockEd25519Key1, - mockSignature1, - 1000, - "nonce1" - ); - - const xdrString = builder.signaturesXDR(); - // Should return valid XDR string (ordering is internal, we just verify it works) - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - }); + it("should get auth requirement args for operation with spend operations", async () => { + const utxoKeys = await generateP256KeyPair(); + const utxo = utxoKeys.publicKey as UTXOPublicKey; + const condition = Condition.create(utxo, validAmount); + const spendOperation = Operation.spend(utxo); + spendOperation.addCondition(condition); - await t.step( - "signaturesXDR should handle both provider and spend signatures", - () => { - const builder = createTestBuilder(); - const mockSignature = Buffer.alloc(64, 0x44); - - // Add spend operation and signatures - builder - .addSpend(mockUTXO1, [mockCreateCondition]) - .addInnerSignature(mockUTXO1, mockSignature, 1000) - .addProviderInnerSignature( - mockEd25519Key1, - mockSignature, - 1000, - "nonce123" - ); + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); - const xdrString = builder.signaturesXDR(); - // Should return valid XDR string with both types - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - } - ); -}); + // deno-lint-ignore no-explicit-any + (testBuilder as any).addSpend(spendOperation); -Deno.test( - "MoonlightTransactionBuilder - High-Level Signing Methods", - async (t) => { - await t.step( - "signWithProvider should sign with provided keypair", - async () => { - const builder = createTestBuilder(); - const keypair = Keypair.random(); - const expirationLedger = 1000; - - await builder.signWithProvider(keypair, expirationLedger); - - // Verify that provider signature was added by checking signaturesXDR doesn't throw - const xdrString = builder.signaturesXDR(); - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - } - ); - - await t.step("signWithProvider should use provided nonce", async () => { - const builder = createTestBuilder(); - const keypair = Keypair.random(); - const expirationLedger = 1000; - const customNonce = "999888777"; - - await builder.signWithProvider(keypair, expirationLedger, customNonce); - - // Should not throw and should generate valid XDR - const xdrString = builder.signaturesXDR(); - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - }); + const args = testBuilder.getAuthRequirementArgs(); - await t.step( - "signWithSpendUtxo should throw error when UTXO not found", - async () => { - const builder = createTestBuilder(); - const mockUtxo = { - publicKey: mockUTXO1, - privateKey: Buffer.alloc(32, 0x01), - signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42), - }; - const expirationLedger = 1000; - - let errorThrown = false; - try { - await builder.signWithSpendUtxo(mockUtxo, expirationLedger); - } catch (error) { - errorThrown = true; - assertEquals( - (error as Error).message, - "No spend operation for this UTXO" - ); - } - assertEquals(errorThrown, true); - } - ); - - await t.step( - "signWithSpendUtxo should sign with UTXO keypair when found", - async () => { - const builder = createTestBuilder(); - const mockUtxo = { - publicKey: mockUTXO1, - privateKey: Buffer.alloc(32, 0x01), - signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42), - }; - const expirationLedger = 1000; + assertExists(args); + assertEquals(Array.isArray(args), true); + assertEquals(args.length, 1); - // Add spend operation first - builder.addSpend(mockUTXO1, [mockCreateCondition]); + // Verify the structure: [scvVec([scvMap(signers)])] + assertEquals(args[0].switch().name, "scvVec"); + const vec = args[0].vec(); + assertEquals(vec?.length, 1); + assertEquals(vec?.[0].switch().name, "scvMap"); + }); - await builder.signWithSpendUtxo(mockUtxo, expirationLedger); + it("should return empty array when no spend operations exist", () => { + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); - // Should not throw - signature was added - assertEquals(true, true); // Test passes if no exception - } - ); + const args = testBuilder.getAuthRequirementArgs(); - await t.step( - "signExtWithEd25519 should sign external auth entry", - async () => { - const builder = createTestBuilder(); - const keypair = Keypair.random(); - const expirationLedger = 1000; + assertExists(args); + assertEquals(Array.isArray(args), true); + assertEquals(args.length, 0); + }); + + it("should get signed operation auth entry with provider signatures", async () => { + const providerKeys = LocalSigner.generateRandom(); + const signatureExpirationLedger = 1000000; + + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); + + await testBuilder.signWithProvider( + providerKeys, + signatureExpirationLedger + ); - // Add deposit operation first - builder.addDeposit(keypair.publicKey() as `G${string}`, 500n, [ - mockDepositCondition, - ]); + const signedEntry = testBuilder.getSignedOperationAuthEntry(); + + assertExists(signedEntry); + assertEquals(typeof signedEntry.toXDR, "function"); + }); + + it("should get signed auth entries including external signatures", async () => { + const providerKeys = LocalSigner.generateRandom(); + const userKeys = LocalSigner.generateRandom(); + const pubKey = userKeys.publicKey() as Ed25519PublicKey; + const condition = Condition.deposit(pubKey, validAmount); + const operation = Operation.deposit(pubKey, validAmount); + operation.addCondition(condition); + + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); + + // deno-lint-ignore no-explicit-any + (testBuilder as any).addDeposit(operation); + + const signatureExpirationLedger = 1000000; + const nonce = generateNonce(); + + await testBuilder.signWithProvider( + providerKeys, + signatureExpirationLedger, + nonce + ); + await testBuilder.signExtWithEd25519( + userKeys, - await builder.signExtWithEd25519(keypair, expirationLedger); + signatureExpirationLedger, + nonce + ); - // Should not throw - external signature was added - assertEquals(true, true); // Test passes if no exception - } - ); + const signedEntries = testBuilder.getSignedAuthEntries(); - await t.step("signExtWithEd25519 should use provided nonce", async () => { - const builder = createTestBuilder(); - const keypair = Keypair.random(); - const expirationLedger = 1000; - const customNonce = "555444333"; + assertExists(signedEntries); + assertEquals(Array.isArray(signedEntries), true); + assertEquals(signedEntries.length >= 2, true); // At least provider + external + }); + }); - // Add deposit operation first - builder.addDeposit(keypair.publicKey() as `G${string}`, 500n, [ - mockDepositCondition, - ]); + describe("Signatures XDR", () => { + it("should generate signatures XDR when provider signatures exist", () => { + const pubKey = + LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + const signature = Buffer.from("provider_signature"); + const expirationLedger = 1000000; + const nonce = generateNonce(); + + const builderWithSigs = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); + + builderWithSigs.addProviderInnerSignature( + pubKey, + signature, + expirationLedger, + nonce + ); - await builder.signExtWithEd25519(keypair, expirationLedger, customNonce); + const signaturesXdr = builderWithSigs.signaturesXDR(); - // Should not throw - custom nonce was used and signature added - assertEquals(true, true); // Test passes if no exception + assertExists(signaturesXdr); + assertEquals(typeof signaturesXdr, "string"); + assertEquals(signaturesXdr.length > 0, true); + }); }); - await t.step("should handle complex signing workflow", async () => { - const builder = createTestBuilder(); - const providerKeypair = Keypair.random(); - const userKeypair = Keypair.random(); - const mockUtxo = { - publicKey: mockUTXO1, - privateKey: Buffer.alloc(32, 0x01), - signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42), - }; - const expirationLedger = 1000; - - // Add operations - builder - .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [ - mockDepositCondition, - ]); - - // Sign with all methods (now that buildAuthPayloadHash is implemented) - await builder.signWithProvider(providerKeypair, expirationLedger); - await builder.signWithSpendUtxo(mockUtxo, expirationLedger); - await builder.signExtWithEd25519(userKeypair, expirationLedger); - - // Should generate valid XDR with all signatures - const xdrString = builder.signaturesXDR(); - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - }); - } -); - -Deno.test("MoonlightTransactionBuilder - Final Methods", async (t) => { - await t.step( - "getSignedAuthEntries should return all signed entries", - async () => { - const builder = createTestBuilder(); - const providerKeypair = Keypair.random(); - const userKeypair = Keypair.random(); - const expirationLedger = 1000; - - // Add operations and sign - builder.addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [ - mockDepositCondition, - ]); - await builder.signWithProvider(providerKeypair, expirationLedger); - await builder.signExtWithEd25519(userKeypair, expirationLedger); - - const signedEntries = builder.getSignedAuthEntries(); - - // Should return an array of signed auth entries - assertEquals(Array.isArray(signedEntries), true); - assertEquals(signedEntries.length, 2); // External + operation entry - } - ); - - await t.step( - "getSignedAuthEntries should include external and operation entries", - async () => { - const builder = createTestBuilder(); - const providerKeypair = Keypair.random(); - const userKeypair = Keypair.random(); - const expirationLedger = 1000; - - // Add operations and sign - builder.addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [ - mockDepositCondition, - ]); - await builder.signWithProvider(providerKeypair, expirationLedger); - await builder.signExtWithEd25519(userKeypair, expirationLedger); - - const signedEntries = builder.getSignedAuthEntries(); - - // Should have both external and operation entries - assertEquals(signedEntries.length >= 2, true); - - // Each entry should be a valid SorobanAuthorizationEntry - for (const entry of signedEntries) { - assertEquals(!!entry, true); - } - } - ); - - await t.step("buildXDR should include all operation types", () => { - const builder = createTestBuilder(); - - // Add one of each operation type - builder - .addCreate(mockUTXO1, 1000n) - .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]) - .addWithdraw(mockEd25519Key2, 300n, [mockWithdrawCondition]); - - const xdr = builder.buildXDR(); - - // Should return valid XDR structure - assertEquals(!!xdr, true); - }); + describe("Invoke Operation", () => { + it("should get invoke operation with all components", async () => { + const providerKeys = LocalSigner.generateRandom(); + const userKeys = LocalSigner.generateRandom(); + const pubKey = userKeys.publicKey() as Ed25519PublicKey; + const condition = Condition.deposit(pubKey, validAmount); + const operation = Operation.deposit(pubKey, validAmount); + operation.addCondition(condition); + + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); + + // deno-lint-ignore no-explicit-any + (testBuilder as any).addDeposit(operation); + + const signatureExpirationLedger = 1000000; + const nonce = generateNonce(); + + await testBuilder.signWithProvider( + providerKeys, + signatureExpirationLedger, + nonce + ); + await testBuilder.signExtWithEd25519( + userKeys, - await t.step("buildXDR should handle empty operations correctly", () => { - const builder = createTestBuilder(); + signatureExpirationLedger, + nonce + ); - // Don't add any operations - const xdr = builder.buildXDR(); + const invokeOp = testBuilder.getInvokeOperation(); - // Should still return valid XDR structure with empty arrays - assertEquals(!!xdr, true); - }); + assertExists(invokeOp); + assertEquals(typeof invokeOp.toXDR, "function"); + }); - await t.step("buildXDR should handle mixed operations", () => { - const builder = createTestBuilder(); + it("should get invoke operation without external signatures", async () => { + const providerKeys = LocalSigner.generateRandom(); + const signatureExpirationLedger = 1000000; - // Add multiple operations of different types - builder - .addCreate(mockUTXO1, 1000n) - .addCreate(mockUTXO2, 2000n) - .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(mockEd25519Key1, 500n, [mockDepositCondition]) - .addDeposit(mockEd25519Key2, 300n, [mockWithdrawCondition]) - .addWithdraw(mockEd25519Key1, 200n, [mockWithdrawCondition]); + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); - const xdr = builder.buildXDR(); + await testBuilder.signWithProvider( + providerKeys, + signatureExpirationLedger + ); - // Should return valid XDR structure - assertEquals(!!xdr, true); - }); + const invokeOp = testBuilder.getInvokeOperation(); - await t.step("should handle complete transaction workflow", async () => { - const builder = createTestBuilder(); - const providerKeypair = Keypair.random(); - const userKeypair = Keypair.random(); - const mockUtxo = { - publicKey: mockUTXO1, - privateKey: Buffer.alloc(32, 0x01), - signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42), - }; - const expirationLedger = 1000; - - // Complete workflow: add operations, sign, and build XDR - builder - .addCreate(mockUTXO1, 1000n) - .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [ - mockDepositCondition, - ]) - .addWithdraw(userKeypair.publicKey() as `G${string}`, 200n, [ - mockWithdrawCondition, - ]); - - // Sign with all methods - await builder.signWithProvider(providerKeypair, expirationLedger); - await builder.signWithSpendUtxo(mockUtxo, expirationLedger); - await builder.signExtWithEd25519(userKeypair, expirationLedger); - - // Get signed entries and build XDR - const signedEntries = builder.getSignedAuthEntries(); - const xdr = builder.buildXDR(); - const xdrString = builder.signaturesXDR(); - - // All should be valid - assertEquals(Array.isArray(signedEntries), true); - assertEquals(signedEntries.length >= 2, true); - assertEquals(!!xdr, true); - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - }); -}); + assertExists(invokeOp); + assertEquals(typeof invokeOp.toXDR, "function"); + }); + }); -Deno.test("Transaction Builder Utility Functions", async (t) => { - await t.step( - "createOpToXDR should convert create operation to XDR correctly", - () => { - const createOp = { - utxo: mockUTXO1, - amount: 1000n, - }; - - const xdr = createOpToXDR(createOp); - - // Should return a valid ScVal - assertEquals(!!xdr, true); - } - ); - - await t.step( - "depositOpToXDR should convert deposit operation to XDR correctly", - () => { - const depositOp = { - pubKey: mockEd25519Key1, - amount: 500n, - conditions: [mockDepositCondition], - }; - - const xdr = depositOpToXDR(depositOp); - - // Should return a valid ScVal - assertEquals(!!xdr, true); - } - ); - - await t.step("depositOpToXDR should handle empty conditions", () => { - const depositOp = { - pubKey: mockEd25519Key1, - amount: 500n, - conditions: [], - }; - - const xdr = depositOpToXDR(depositOp); - - // Should return a valid ScVal even with empty conditions - assertEquals(!!xdr, true); + describe("Signatures XDR", () => { + // ... existing signatures XDR tests ... + }); }); - await t.step( - "withdrawOpToXDR should convert withdraw operation to XDR correctly", - () => { - const withdrawOp = { - pubKey: mockEd25519Key1, - amount: 300n, - conditions: [mockWithdrawCondition], - }; - - const xdr = withdrawOpToXDR(withdrawOp); - - // Should return a valid ScVal - assertEquals(!!xdr, true); - } - ); - - await t.step("withdrawOpToXDR should handle empty conditions", () => { - const withdrawOp = { - pubKey: mockEd25519Key1, - amount: 300n, - conditions: [], - }; - - const xdr = withdrawOpToXDR(withdrawOp); - - // Should return a valid ScVal even with empty conditions - assertEquals(!!xdr, true); - }); + describe("Errors", () => { + describe("Operation Validation", () => { + it("should throw error for duplicate CREATE operations", async () => { + const utxo = (await generateP256KeyPair()).publicKey as UTXOPublicKey; + const operation1 = Operation.create(utxo, validAmount); + // Note: CREATE operations don't have conditions + const operation2 = Operation.create(utxo, validAmount + 100n); - await t.step( - "spendOpToXDR should convert spend operation to XDR correctly", - () => { - const spendOp = { - utxo: mockUTXO1, - conditions: [mockCreateCondition, mockDepositCondition], - }; + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); - const xdr = spendOpToXDR(spendOp); + testBuilder.addOperation(operation1); - // Should return a valid ScVal - assertEquals(!!xdr, true); - } - ); + assertThrows( + () => testBuilder.addOperation(operation2), + Error, + "Create operation for this UTXO already exists" + ); + }); + + it("should throw error for duplicate DEPOSIT operations", () => { + const pubKey = + LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + const condition = Condition.deposit(pubKey, validAmount); + const operation1 = Operation.deposit(pubKey, validAmount); + operation1.addCondition(condition); + const operation2 = Operation.deposit(pubKey, validAmount + 100n); + operation2.addCondition(condition); + + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); + + // deno-lint-ignore no-explicit-any + (testBuilder as any).addDeposit(operation1); - await t.step("spendOpToXDR should handle empty conditions", () => { - const spendOp = { - utxo: mockUTXO1, - conditions: [], - }; + assertThrows( + // deno-lint-ignore no-explicit-any + () => (testBuilder as any).addDeposit(operation2), + Error, + "Deposit operation for this public key already exists" + ); + }); + + it("should throw error for duplicate WITHDRAW operations", () => { + const pubKey = + LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + const condition = Condition.withdraw(pubKey, validAmount); + const operation1 = Operation.withdraw(pubKey, validAmount); + operation1.addCondition(condition); + const operation2 = Operation.withdraw(pubKey, validAmount + 100n); + operation2.addCondition(condition); + + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); + + // deno-lint-ignore no-explicit-any + (testBuilder as any).addWithdraw(operation1); - const xdr = spendOpToXDR(spendOp); + assertThrows( + // deno-lint-ignore no-explicit-any + () => (testBuilder as any).addWithdraw(operation2), + Error, + "Withdraw operation for this public key already exists" + ); + }); - // Should return a valid ScVal even with empty conditions - assertEquals(!!xdr, true); - }); + it("should throw error for zero amount in CREATE operation", async () => { + const utxo = (await generateP256KeyPair()).publicKey as UTXOPublicKey; - await t.step("all utility functions should handle different amounts", () => { - // Test createOpToXDR with different amounts - const createOp1 = { utxo: mockUTXO1, amount: 1n }; - const createOp2 = { utxo: mockUTXO2, amount: 999999999n }; + assertThrows( + () => Operation.create(utxo, 0n), + Error, + "Amount must be greater than zero" + ); + }); - const xdr1 = createOpToXDR(createOp1); - const xdr2 = createOpToXDR(createOp2); + it("should throw error for negative amount in DEPOSIT operation", () => { + const pubKey = + LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; - assertEquals(!!xdr1, true); - assertEquals(!!xdr2, true); + assertThrows( + () => Operation.deposit(pubKey, -100n), + Error, + "Amount must be greater than zero" + ); + }); - // Test depositOpToXDR with different amounts - const depositOp1 = { pubKey: mockEd25519Key1, amount: 1n, conditions: [] }; - const depositOp2 = { - pubKey: mockEd25519Key2, - amount: 999999999n, - conditions: [], - }; + it("should throw error for negative amount in WITHDRAW operation", () => { + const pubKey = + LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; - const xdr3 = depositOpToXDR(depositOp1); - const xdr4 = depositOpToXDR(depositOp2); + assertThrows( + () => Operation.withdraw(pubKey, -100n), + Error, + "Amount must be greater than zero" + ); + }); + }); - assertEquals(!!xdr3, true); - assertEquals(!!xdr4, true); + describe("Signature Validation", () => { + it("should throw error when adding inner signature without spend operation", async () => { + const signature = Buffer.from("test_signature"); + const expirationLedger = 1000000; + const nonExistentUtxo = (await generateP256KeyPair()) + .publicKey as UTXOPublicKey; - // Test withdrawOpToXDR with different amounts - const withdrawOp1 = { pubKey: mockEd25519Key1, amount: 1n, conditions: [] }; - const withdrawOp2 = { - pubKey: mockEd25519Key2, - amount: 999999999n, - conditions: [], - }; + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); - const xdr5 = withdrawOpToXDR(withdrawOp1); - const xdr6 = withdrawOpToXDR(withdrawOp2); + assertThrows( + () => + testBuilder.addInnerSignature( + nonExistentUtxo, + signature, + expirationLedger + ), + Error, + "No spend operation for this UTXO" + ); + }); - assertEquals(!!xdr5, true); - assertEquals(!!xdr6, true); - }); + it("should throw error when adding external signature without deposit/withdraw operation", () => { + // deno-lint-ignore no-explicit-any + const mockAuthEntry = {} as any; + const nonExistentKey = + LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; - await t.step("utility functions should handle multiple conditions", () => { - // Test with multiple conditions - const multipleConditions = [ - mockCreateCondition, - mockDepositCondition, - mockWithdrawCondition, - ]; - - const depositOp = { - pubKey: mockEd25519Key1, - amount: 500n, - conditions: multipleConditions, - }; - - const withdrawOp = { - pubKey: mockEd25519Key2, - amount: 300n, - conditions: multipleConditions, - }; - - const spendOp = { - utxo: mockUTXO1, - conditions: multipleConditions, - }; - - const depositXdr = depositOpToXDR(depositOp); - const withdrawXdr = withdrawOpToXDR(withdrawOp); - const spendXdr = spendOpToXDR(spendOp); - - assertEquals(!!depositXdr, true); - assertEquals(!!withdrawXdr, true); - assertEquals(!!spendXdr, true); - }); -}); + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); -Deno.test( - "MoonlightTransactionBuilder - Integration and Edge Cases", - async (t) => { - await t.step( - "should build complete transaction with all operation types", - async () => { - const builder = createTestBuilder(); - const providerKeypair = Keypair.random(); - const userKeypair1 = Keypair.random(); - const userKeypair2 = Keypair.random(); - const mockUtxo1 = { - publicKey: mockUTXO1, - privateKey: Buffer.alloc(32, 0x01), - signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42), - }; - const mockUtxo2 = { - publicKey: mockUTXO2, - privateKey: Buffer.alloc(32, 0x02), - signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x43), - }; - const expirationLedger = 1000; - - // Add all types of operations - builder - .addCreate(mockUTXO1, 1000n) - .addCreate(mockUTXO2, 2000n) - .addSpend(mockUTXO1, [mockCreateCondition]) - .addSpend(mockUTXO2, [mockDepositCondition, mockWithdrawCondition]) - .addDeposit(userKeypair1.publicKey() as `G${string}`, 500n, [ - mockDepositCondition, - ]) - .addDeposit(userKeypair2.publicKey() as `G${string}`, 300n, [ - mockWithdrawCondition, - ]) - .addWithdraw(userKeypair1.publicKey() as `G${string}`, 200n, [ - mockWithdrawCondition, - ]) - .addWithdraw(userKeypair2.publicKey() as `G${string}`, 100n, [ - mockCreateCondition, - ]); - - // Sign with all methods - await builder.signWithProvider(providerKeypair, expirationLedger); - await builder.signWithSpendUtxo(mockUtxo1, expirationLedger); - await builder.signWithSpendUtxo(mockUtxo2, expirationLedger); - await builder.signExtWithEd25519(userKeypair1, expirationLedger); - await builder.signExtWithEd25519(userKeypair2, expirationLedger); - - // Verify all components work together - const operations = builder.getOperation(); - const signedEntries = builder.getSignedAuthEntries(); - const xdr = builder.buildXDR(); - const xdrString = builder.signaturesXDR(); + assertThrows( + () => testBuilder.addExtSignedEntry(nonExistentKey, mockAuthEntry), + Error, + "No deposit or withdraw operation for this public key" + ); + }); - // Validate operations - assertEquals(operations.create.length, 2); - assertEquals(operations.spend.length, 2); - assertEquals(operations.deposit.length, 2); - assertEquals(operations.withdraw.length, 2); + it("should throw error when generating signatures XDR without provider signatures", () => { + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); - // Validate signatures - assertEquals(Array.isArray(signedEntries), true); - assertEquals(signedEntries.length >= 3, true); // Provider + 2 external - - // Validate XDR - assertEquals(!!xdr, true); - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - } - ); - - await t.step( - "should handle complex transaction with multiple signatures", - async () => { - const builder = createTestBuilder(); - const providerKeypair1 = Keypair.random(); - const providerKeypair2 = Keypair.random(); - const userKeypair1 = Keypair.random(); - const userKeypair2 = Keypair.random(); - const userKeypair3 = Keypair.random(); - const expirationLedger = 1000; - - // Add operations - each user keypair needs both deposit and withdraw operations - builder - .addCreate(mockUTXO1, 1000n) - .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(userKeypair1.publicKey() as `G${string}`, 500n, [ - mockDepositCondition, - ]) - .addDeposit(userKeypair2.publicKey() as `G${string}`, 300n, [ - mockWithdrawCondition, - ]) - .addDeposit(userKeypair3.publicKey() as `G${string}`, 200n, [ - mockWithdrawCondition, - ]) - .addWithdraw(userKeypair1.publicKey() as `G${string}`, 200n, [ - mockWithdrawCondition, - ]) - .addWithdraw(userKeypair2.publicKey() as `G${string}`, 100n, [ - mockCreateCondition, - ]) - .addWithdraw(userKeypair3.publicKey() as `G${string}`, 150n, [ - mockDepositCondition, - ]); - - // Add multiple provider signatures - await builder.signWithProvider(providerKeypair1, expirationLedger); - await builder.signWithProvider(providerKeypair2, expirationLedger); - - // Add multiple external signatures (each keypair has both deposit and withdraw) - await builder.signExtWithEd25519(userKeypair1, expirationLedger); - await builder.signExtWithEd25519(userKeypair2, expirationLedger); - await builder.signExtWithEd25519(userKeypair3, expirationLedger); - - // Verify multiple signatures are handled correctly - const signedEntries = builder.getSignedAuthEntries(); - const xdrString = builder.signaturesXDR(); + assertThrows( + () => testBuilder.signaturesXDR(), + Error, + "No Provider signatures added" + ); + }); - assertEquals(Array.isArray(signedEntries), true); - assertEquals(signedEntries.length >= 4, true); // 2 providers + 3 external - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - } - ); - - await t.step("should validate transaction integrity", async () => { - const builder = createTestBuilder(); - const providerKeypair = Keypair.random(); - const userKeypair = Keypair.random(); - const mockUtxo = { - publicKey: mockUTXO1, - privateKey: Buffer.alloc(32, 0x01), - signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42), - }; - const expirationLedger = 1000; - - // Build transaction - builder - .addCreate(mockUTXO1, 1000n) - .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [ - mockDepositCondition, - ]); - - await builder.signWithProvider(providerKeypair, expirationLedger); - await builder.signWithSpendUtxo(mockUtxo, expirationLedger); - await builder.signExtWithEd25519(userKeypair, expirationLedger); - - // Verify transaction integrity - const operations = builder.getOperation(); - const signedEntries = builder.getSignedAuthEntries(); - const xdr = builder.buildXDR(); - const xdrString = builder.signaturesXDR(); - - // All components should be consistent - assertEquals(operations.create.length, 1); - assertEquals(operations.spend.length, 1); - assertEquals(operations.deposit.length, 1); - assertEquals(operations.withdraw.length, 0); - - assertEquals(Array.isArray(signedEntries), true); - assertEquals(signedEntries.length >= 2, true); - assertEquals(!!xdr, true); - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - }); + it("should throw error when getting signed operation auth entry without provider signatures", () => { + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); - await t.step("should handle maximum number of operations", () => { - const builder = createTestBuilder(); - const maxOperations = 10; - - // Add maximum number of each operation type - for (let i = 0; i < maxOperations; i++) { - const utxo = new Uint8Array([ - i, - i + 1, - i + 2, - i + 3, - i + 4, - i + 5, - i + 6, - i + 7, - ]); - const keypair = Keypair.random(); - - builder - .addCreate(utxo, BigInt(1000 + i)) - .addSpend(utxo, [mockCreateCondition]) - .addDeposit(keypair.publicKey() as `G${string}`, BigInt(500 + i), [ - mockDepositCondition, - ]) - .addWithdraw(keypair.publicKey() as `G${string}`, BigInt(300 + i), [ - mockWithdrawCondition, - ]); - } - - const operations = builder.getOperation(); - const xdr = builder.buildXDR(); - - assertEquals(operations.create.length, maxOperations); - assertEquals(operations.spend.length, maxOperations); - assertEquals(operations.deposit.length, maxOperations); - assertEquals(operations.withdraw.length, maxOperations); - assertEquals(!!xdr, true); + assertThrows( + () => testBuilder.getSignedOperationAuthEntry(), + Error, + "No Provider signatures added" + ); + }); }); - await t.step("should handle edge cases with zero amounts", () => { - const builder = createTestBuilder(); - - // These should throw errors for zero amounts - assertThrows( - () => builder.addCreate(mockUTXO1, 0n), - Error, - "Create operation amount must be positive" - ); - - assertThrows( - () => builder.addDeposit(mockEd25519Key1, 0n, []), - Error, - "Deposit operation amount must be positive" - ); - - assertThrows( - () => builder.addWithdraw(mockEd25519Key1, 0n, []), - Error, - "Withdraw operation amount must be positive" - ); - }); + describe("External Auth Entry Validation", () => { + it("should throw error when getting external auth entry for non-existent deposit", () => { + const nonExistentKey = + LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + const nonce = generateNonce(); + const signatureExpirationLedger = 1000000; - await t.step("should handle edge cases with negative amounts", () => { - const builder = createTestBuilder(); - - // These should throw errors for negative amounts - assertThrows( - () => builder.addCreate(mockUTXO1, -100n), - Error, - "Create operation amount must be positive" - ); - - assertThrows( - () => builder.addDeposit(mockEd25519Key1, -100n, []), - Error, - "Deposit operation amount must be positive" - ); - - assertThrows( - () => builder.addWithdraw(mockEd25519Key1, -100n, []), - Error, - "Withdraw operation amount must be positive" - ); - }); + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + asset, + network, + }); - await t.step("should handle invalid input parameters", () => { - const builder = createTestBuilder(); - - // Test with empty UTXO array - should work but be a valid UTXO - const emptyUtxo = new Uint8Array(8).fill(0); - builder.addCreate(emptyUtxo, 1000n); - - // Try to add the same UTXO again - should throw error - assertThrows( - () => builder.addCreate(emptyUtxo, 2000n), - Error, - "Create operation for this UTXO already exists" - ); - - // Test with empty public key - should work but be a valid key - const emptyKey = ("G" + "A".repeat(55)) as `G${string}`; // Valid format but empty content - builder.addDeposit(emptyKey, 500n, []); - - // Try to add the same public key again - should throw error - assertThrows( - () => builder.addDeposit(emptyKey, 1000n, []), - Error, - "Deposit operation for this public key already exists" - ); + assertThrows( + () => + testBuilder.getExtAuthEntry( + nonExistentKey, + nonce, + signatureExpirationLedger + ), + Error, + "No deposit operation for this address" + ); + }); }); - await t.step("should handle concurrent operations", async () => { - const builder = createTestBuilder(); - const providerKeypair = Keypair.random(); - const userKeypair = Keypair.random(); - const mockUtxo = { - publicKey: mockUTXO1, - privateKey: Buffer.alloc(32, 0x01), - signPayload: async (payload: Uint8Array) => Buffer.alloc(64, 0x42), - }; - const expirationLedger = 1000; - - // Add operations - builder - .addCreate(mockUTXO1, 1000n) - .addSpend(mockUTXO1, [mockCreateCondition]) - .addDeposit(userKeypair.publicKey() as `G${string}`, 500n, [ - mockDepositCondition, - ]); - - // Sign concurrently (simulate concurrent access) - const signingPromises = [ - builder.signWithProvider(providerKeypair, expirationLedger), - builder.signWithSpendUtxo(mockUtxo, expirationLedger), - builder.signExtWithEd25519(userKeypair, expirationLedger), - ]; - - await Promise.all(signingPromises); - - // Verify all signatures were added - const signedEntries = builder.getSignedAuthEntries(); - const xdrString = builder.signaturesXDR(); - - assertEquals(Array.isArray(signedEntries), true); - assertEquals(signedEntries.length >= 2, true); - assertEquals(typeof xdrString, "string"); - assertEquals(xdrString.length > 0, true); - }); + describe("Property Access Validation", () => { + it("should throw error when accessing unset properties", () => { + const emptyBuilder = Object.create( + MoonlightTransactionBuilder.prototype + ); - await t.step("should handle large transaction data", () => { - const builder = createTestBuilder(); - const largeAmount = 999999999999999999n; // Very large amount - const largeUtxo = new Uint8Array(64).fill(0xff); // Large UTXO - const keypair = Keypair.random(); - - // Add operations with large data - builder - .addCreate(largeUtxo, largeAmount) - .addSpend(largeUtxo, [ - mockCreateCondition, - mockDepositCondition, - mockWithdrawCondition, - ]) - .addDeposit(keypair.publicKey() as `G${string}`, largeAmount, [ - mockDepositCondition, - ]) - .addWithdraw(keypair.publicKey() as `G${string}`, largeAmount, [ - mockWithdrawCondition, - ]); - - const operations = builder.getOperation(); - const xdr = builder.buildXDR(); - - assertEquals(operations.create.length, 1); - assertEquals(operations.create[0].amount, largeAmount); - assertEquals(operations.spend.length, 1); - assertEquals(operations.deposit.length, 1); - assertEquals(operations.deposit[0].amount, largeAmount); - assertEquals(operations.withdraw.length, 1); - assertEquals(operations.withdraw[0].amount, largeAmount); - assertEquals(!!xdr, true); + assertThrows( + // deno-lint-ignore no-explicit-any + () => (emptyBuilder as any).require("_channelId"), + Error, + "Property _channelId is not set in the Transaction Builder instance" + ); + }); }); - } -); + }); +}); diff --git a/src/transaction-builder/utils/ordering.ts b/src/transaction-builder/utils/ordering.ts index 3d7c58f..1d682e5 100644 --- a/src/transaction-builder/utils/ordering.ts +++ b/src/transaction-builder/utils/ordering.ts @@ -1,10 +1,8 @@ import { Buffer } from "buffer"; -import { SpendOperation } from "../types.ts"; +import type { SpendOperation } from "../../operation/types.ts"; export const orderSpendByUtxo = (spend: SpendOperation[]): SpendOperation[] => { return [...spend].sort((a, b) => - Buffer.from(a.utxo).compare(Buffer.from(b.utxo)) + Buffer.from(a.getUtxo()).compare(Buffer.from(b.getUtxo())) ); }; - - diff --git a/src/transaction-builder/validators/operations.ts b/src/transaction-builder/validators/operations.ts index 9165017..5f7c0c5 100644 --- a/src/transaction-builder/validators/operations.ts +++ b/src/transaction-builder/validators/operations.ts @@ -1,42 +1,48 @@ import { Buffer } from "buffer"; -import { Condition } from "../../conditions/types.ts"; -import { UTXOPublicKey, Ed25519PublicKey } from "../types.ts"; +import type { UTXOPublicKey } from "../../core/utxo-keypair-base/types.ts"; +import type { Ed25519PublicKey } from "@colibri/core"; export const assertPositiveAmount = (amount: bigint, context: string) => { if (amount <= 0n) throw new Error(`${context} amount must be positive`); }; export const assertNoDuplicateCreate = ( - existing: { utxo: UTXOPublicKey }[], - utxo: UTXOPublicKey, + existing: { getUtxo(): UTXOPublicKey }[], + op: { getUtxo(): UTXOPublicKey } ) => { - if (existing.find((c) => Buffer.from(c.utxo).equals(Buffer.from(utxo)))) + if ( + existing.find((c) => + Buffer.from(c.getUtxo()).equals(Buffer.from(op.getUtxo())) + ) + ) throw new Error("Create operation for this UTXO already exists"); }; export const assertNoDuplicateSpend = ( - existing: { utxo: UTXOPublicKey }[], - utxo: UTXOPublicKey, + existing: { getUtxo(): UTXOPublicKey }[], + op: { getUtxo(): UTXOPublicKey } ) => { - if (existing.find((s) => Buffer.from(s.utxo).equals(Buffer.from(utxo)))) + if ( + existing.find((s) => + Buffer.from(s.getUtxo()).equals(Buffer.from(op.getUtxo())) + ) + ) throw new Error("Spend operation for this UTXO already exists"); }; export const assertNoDuplicatePubKey = ( - existing: { pubKey: Ed25519PublicKey }[], - pubKey: Ed25519PublicKey, - context: string, + existing: { getPublicKey(): Ed25519PublicKey }[], + op: { getPublicKey(): Ed25519PublicKey }, + context: string ) => { - if (existing.find((d) => d.pubKey === pubKey)) + if (existing.find((d) => d.getPublicKey() === op.getPublicKey())) throw new Error(`${context} operation for this public key already exists`); }; export const assertSpendExists = ( - existing: { utxo: UTXOPublicKey; conditions: Condition[] }[], - utxo: UTXOPublicKey, + existing: { getUtxo(): UTXOPublicKey }[], + utxo: UTXOPublicKey ) => { - if (!existing.find((s) => Buffer.from(s.utxo).equals(Buffer.from(utxo)))) + if (!existing.find((s) => Buffer.from(s.getUtxo()).equals(Buffer.from(utxo)))) throw new Error("No spend operation for this UTXO"); }; - - diff --git a/test/integration/privacy-channel.integration.test.ts b/test/integration/privacy-channel.integration.test.ts index 9ed1a37..380e4c6 100644 --- a/test/integration/privacy-channel.integration.test.ts +++ b/test/integration/privacy-channel.integration.test.ts @@ -35,9 +35,9 @@ import { PrivacyChannel } from "../../src/privacy-channel/index.ts"; import { disableSanitizeConfig } from "../utils/disable-sanitize-config.ts"; import { generateP256KeyPair } from "../../src/utils/secp256r1/generateP256KeyPair.ts"; import { MoonlightTransactionBuilder } from "../../src/transaction-builder/index.ts"; -import { Condition } from "../../src/conditions/index.ts"; import { Server } from "@stellar/stellar-sdk/rpc"; import { generateNonce } from "../../src/utils/common/index.ts"; +import { MoonlightOperation as op } from "../../src/operation/index.ts"; describe( "[Testnet - Integration] PrivacyChannel", @@ -238,13 +238,16 @@ describe( asset: Asset.native(), }); - depositTx.addDeposit(john.address() as Ed25519PublicKey, 500n, [ - Condition.create(utxoAKeypair.publicKey, 250n), - Condition.create(utxoBKeypair.publicKey, 250n), - ]); + const createOpA = op.create(utxoAKeypair.publicKey, 250n); + const createOpB = op.create(utxoBKeypair.publicKey, 250n); - depositTx.addCreate(utxoAKeypair.publicKey, 250n); - depositTx.addCreate(utxoBKeypair.publicKey, 250n); + depositTx.addOperation(createOpA); + depositTx.addOperation(createOpB); + depositTx.addOperation( + op + .deposit(john.address() as Ed25519PublicKey, 500n) + .addConditions([createOpA.toCondition(), createOpB.toCondition()]) + ); const latestLedger = await rpc.getLatestLedger(); From 875a0312c3840cfb0491dd99e4ad5132c2c84262 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Tue, 21 Oct 2025 09:07:58 -0300 Subject: [PATCH 53/90] refactor: remove unused XDR operation files for cleaner codebase --- src/transaction-builder/xdr/index.ts | 1 - src/transaction-builder/xdr/ops-to-xdr.ts | 44 ----------------------- 2 files changed, 45 deletions(-) delete mode 100644 src/transaction-builder/xdr/index.ts delete mode 100644 src/transaction-builder/xdr/ops-to-xdr.ts diff --git a/src/transaction-builder/xdr/index.ts b/src/transaction-builder/xdr/index.ts deleted file mode 100644 index 51a5bd0..0000000 --- a/src/transaction-builder/xdr/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./ops-to-xdr.ts"; diff --git a/src/transaction-builder/xdr/ops-to-xdr.ts b/src/transaction-builder/xdr/ops-to-xdr.ts deleted file mode 100644 index adc256c..0000000 --- a/src/transaction-builder/xdr/ops-to-xdr.ts +++ /dev/null @@ -1,44 +0,0 @@ -// import { xdr, nativeToScVal } from "@stellar/stellar-sdk"; -// import { Buffer } from "buffer"; -// import type { -// CreateOperation, -// DepositOperation, -// WithdrawOperation, -// SpendOperation, -// } from "../types.ts"; - -// export const createOpToXDR = (op: CreateOperation): xdr.ScVal => { -// return xdr.ScVal.scvVec([ -// xdr.ScVal.scvBytes(Buffer.from(op.utxo as Uint8Array)), -// nativeToScVal(op.amount, { type: "i128" }), -// ]); -// }; - -// export const depositOpToXDR = (op: DepositOperation): xdr.ScVal => { -// return xdr.ScVal.scvVec([ -// nativeToScVal(op.pubKey, { type: "address" }), -// nativeToScVal(op.amount, { type: "i128" }), -// op.conditions.length === 0 -// ? xdr.ScVal.scvVec(null) -// : xdr.ScVal.scvVec(op.conditions.map((c) => c.toScVal())), -// ]); -// }; - -// export const withdrawOpToXDR = (op: WithdrawOperation): xdr.ScVal => { -// return xdr.ScVal.scvVec([ -// nativeToScVal(op.pubKey, { type: "address" }), -// nativeToScVal(op.amount, { type: "i128" }), -// op.conditions.length === 0 -// ? xdr.ScVal.scvVec(null) -// : xdr.ScVal.scvVec(op.conditions.map((c) => c.toScVal())), -// ]); -// }; - -// export const spendOpToXDR = (op: SpendOperation): xdr.ScVal => { -// return xdr.ScVal.scvVec([ -// xdr.ScVal.scvBytes(Buffer.from(op.utxo as Uint8Array)), -// op.conditions.length === 0 -// ? xdr.ScVal.scvVec(null) -// : xdr.ScVal.scvVec(op.conditions.map((c) => c.toScVal())), -// ]); -// }; From 9d222056036222b7b0d5348ba87c22dade93f722 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:50:36 -0300 Subject: [PATCH 54/90] Update src/operation/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/operation/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/operation/index.ts b/src/operation/index.ts index 150393b..973f482 100644 --- a/src/operation/index.ts +++ b/src/operation/index.ts @@ -36,7 +36,9 @@ export class MoonlightOperation implements BaseOperation { throw new Error("Amount must be greater than zero"); } - // Only Create operations can't have conditions + // Business rule: CREATE operations cannot have conditions. + // This is because conditions are only applicable to DEPOSIT, SPEND, and WITHDRAW operations. + // Attempting to add conditions to CREATE would violate the intended operation semantics. if (op !== UTXOOperationType.CREATE) this.setConditions([]); this._op = op; From 2e02d98d394ba4a8ab69fb9b32a198e99c831558 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:50:51 -0300 Subject: [PATCH 55/90] Update src/operation/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/operation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/operation/index.ts b/src/operation/index.ts index 973f482..c5957b6 100644 --- a/src/operation/index.ts +++ b/src/operation/index.ts @@ -118,7 +118,7 @@ export class MoonlightOperation implements BaseOperation { | Ed25519PublicKey | UTXOPublicKey | ConditionType[] { - if (this[arg]) return this[arg]; + if (this[arg] !== undefined) return this[arg]; throw new Error(`Property ${arg} is not set in the Operation instance`); } From bc22fddf0c437d56b07fce2f5fc6ad9cec411031 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:51:02 -0300 Subject: [PATCH 56/90] Update src/transaction-builder/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/transaction-builder/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transaction-builder/index.ts b/src/transaction-builder/index.ts index 2d7d0b1..6e2bbdc 100644 --- a/src/transaction-builder/index.ts +++ b/src/transaction-builder/index.ts @@ -132,7 +132,7 @@ export class MoonlightTransactionBuilder { | Map | Map | Map { - if (this[arg]) return this[arg]; + if (this[arg] !== undefined) return this[arg]; throw new Error( `Property ${arg} is not set in the Transaction Builder instance` ); From cd8a0c6a0201654f332996c2ce43094fc26e2fd2 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Tue, 21 Oct 2025 09:53:47 -0300 Subject: [PATCH 57/90] fix: update conditionsToScVal method to return an empty array instead of null when no conditions are present --- src/operation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/operation/index.ts b/src/operation/index.ts index c5957b6..3e575f4 100644 --- a/src/operation/index.ts +++ b/src/operation/index.ts @@ -390,7 +390,7 @@ export class MoonlightOperation implements BaseOperation { private conditionsToScVal(): xdr.ScVal { return this.hasConditions() ? xdr.ScVal.scvVec(this.getConditions().map((c) => c.toScVal())) - : xdr.ScVal.scvVec(null); + : xdr.ScVal.scvVec([]); } private createToScVal(): xdr.ScVal { From d901b4992ab9c8cee3a84230db51332f26493da8 Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Wed, 22 Oct 2025 17:15:53 -0300 Subject: [PATCH 58/90] test: Add integration tests for the UTXO account management and the Moonlight operations --- src/derivation/stellar/stellar-network-id.ts | 8 +- src/operation/types.ts | 4 +- .../utxo-based-account.integration.test.ts | 1409 +++++++++++------ 3 files changed, 964 insertions(+), 457 deletions(-) diff --git a/src/derivation/stellar/stellar-network-id.ts b/src/derivation/stellar/stellar-network-id.ts index 9f53097..fa17639 100644 --- a/src/derivation/stellar/stellar-network-id.ts +++ b/src/derivation/stellar/stellar-network-id.ts @@ -1,5 +1,7 @@ +import { NetworkPassphrase } from "@colibri/core"; + export enum StellarNetworkId { - Mainnet = "Public Global Stellar Network ; September 2015", - Testnet = "Test SDF Network ; September 2015", - Futurenet = "Future Stellar Network ; September 2015", + Mainnet = NetworkPassphrase.MAINNET, + Testnet = NetworkPassphrase.TESTNET, + Futurenet = NetworkPassphrase.FUTURENET, } diff --git a/src/operation/types.ts b/src/operation/types.ts index 5587227..8e7a92c 100644 --- a/src/operation/types.ts +++ b/src/operation/types.ts @@ -11,8 +11,8 @@ import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; export enum UTXOOperationType { CREATE = "Create", - DEPOSIT = "Deposit", - WITHDRAW = "Withdraw", + DEPOSIT = "ExtDeposit", + WITHDRAW = "ExtWithdraw", SPEND = "Spend", } diff --git a/test/integration/utxo-based-account.integration.test.ts b/test/integration/utxo-based-account.integration.test.ts index 5c84598..881d496 100644 --- a/test/integration/utxo-based-account.integration.test.ts +++ b/test/integration/utxo-based-account.integration.test.ts @@ -1,452 +1,957 @@ -// // deno-lint-ignore-file require-await -// import { assertEquals, assertExists } from "@std/assert"; -// import { Buffer } from "buffer"; -// import { -// type PoolEngine, -// ReadMethods, -// WriteMethods, -// UtxoBasedStellarAccount, -// UTXOStatus, -// } from "../../mod.ts"; -// import { createTestAccount } from "../helpers/create-test-account.ts"; -// import { createTxInvocation } from "../helpers/create-tx-invocation.ts"; -// import { deployPrivacyPool } from "../helpers/deploy-pool.ts"; -// import type { SorobanTransactionPipelineOutputVerbose } from "stellar-plus/lib/stellar-plus/core/pipelines/soroban-transaction/types"; - -// // Testnet XLM contract ID -// const XLM_CONTRACT_ID_TESTNET = -// "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; - -// Deno.test("UTXOBasedAccount Integration Tests", async (t) => { -// const { account, networkConfig } = await createTestAccount(); -// const txInvocation = createTxInvocation(account); -// let poolEngine: PoolEngine; -// let utxoAccount: UtxoBasedStellarAccount; -// const depositAmount = 500000n; // 0.05 XLM -// const testRoot = "S-TEST_SECRET_ROOT"; - -// // Setup test environment -// await t.step("setup: deploy privacy pool contract", async () => { -// poolEngine = await deployPrivacyPool({ -// admin: account, -// assetContractId: XLM_CONTRACT_ID_TESTNET, -// networkConfig, -// }); - -// assertExists(poolEngine, "Pool engine should be initialized"); -// assertExists(poolEngine.getContractId(), "Contract ID should be generated"); -// }); - -// // Initialize UTXOBasedAccount with pool engine's derivator - directly in the test -// await t.step("setup: create UTXOBasedAccount instance", async () => { -// const derivator = poolEngine.derivator; -// assertExists(derivator, "Pool engine should have a derivator"); - -// // Create UTXOBasedAccount directly in the test with a balance fetching function -// utxoAccount = new UtxoBasedStellarAccount({ -// derivator, -// root: testRoot, -// options: { -// batchSize: 10, -// fetchBalances: async (publicKeys: Uint8Array[]) => { -// return poolEngine.read({ -// ...txInvocation, -// method: ReadMethods.balances, -// methodArgs: { -// utxos: publicKeys.map((pk) => Buffer.from(pk)), -// }, -// }); -// }, -// }, -// }); - -// assertExists(utxoAccount, "UTXOBasedAccount should be initialized"); -// }); - -// // Derive a batch of UTXOs -// await t.step("should derive a batch of UTXOs", async () => { -// const batchSize = 5; -// await utxoAccount.deriveBatch({ startIndex: 0, count: batchSize }); - -// const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); -// assertEquals( -// freeUtxos.length, -// batchSize, -// "Should have derived the correct number of UTXOs" -// ); - -// // Verify each UTXO has required properties -// for (const utxo of freeUtxos) { -// assertExists(utxo.publicKey, "UTXO should have a public key"); -// assertExists(utxo.privateKey, "UTXO should have a private key"); - -// // Verify the keypair can sign data -// const testData = new Uint8Array(32); -// crypto.getRandomValues(testData); -// const signature = await utxo.signPayload(testData); -// assertExists(signature, "Should be able to generate a signature"); -// } -// }); - -// // Deposit into a UTXO managed by the account -// await t.step("should deposit to a UTXO and update its state", async () => { -// // Get a free UTXO -// const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); - -// const testUtxo = freeUtxos[0]; -// assertExists(testUtxo, "Should have at least one free UTXO"); - -// // We know the UTXOs are indexed starting from 0, since we derived them that way -// const utxoIndex = 0; - -// // Deposit to the UTXO -// const depositResult = (await poolEngine.write({ -// ...txInvocation, -// method: WriteMethods.deposit, -// methodArgs: { -// from: account.getPublicKey(), -// amount: depositAmount, -// utxo: Buffer.from(testUtxo.publicKey), -// }, -// options: { verboseOutput: true, includeHashOutput: true }, -// })) as SorobanTransactionPipelineOutputVerbose; - -// assertExists( -// depositResult.sorobanTransactionOutput, -// "Deposit transaction result should exist" -// ); - -// // Mark the UTXO as having a balance (would normally be done by batchLoad) -// utxoAccount.updateUTXOState(utxoIndex, UTXOStatus.UNSPENT); - -// // Verify the balance through the contract -// const balanceResult = await poolEngine.read({ -// ...txInvocation, -// method: ReadMethods.balance, -// methodArgs: { -// utxo: Buffer.from(testUtxo.publicKey), -// }, -// }); - -// assertEquals( -// balanceResult, -// depositAmount, -// "UTXO balance should match the deposited amount" -// ); -// }); - -// // Test batch loading of UTXOs -// await t.step("should batch load UTXOs with balances", async () => { -// // Load all UTXOs and check their states -// await utxoAccount.batchLoad(); - -// // Should have at least one UNSPENT UTXO -// const unspentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); -// assertEquals( -// unspentUtxos.length >= 1, -// true, -// "Should have at least one UNSPENT UTXO after batch loading" -// ); - -// // Other UTXOs should be marked as SPENT or FREE -// const allUtxos = [ -// ...utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT), -// ...utxoAccount.getUTXOsByState(UTXOStatus.SPENT), -// ...utxoAccount.getUTXOsByState(UTXOStatus.FREE), -// ]; - -// // We derived 5 UTXOs, so all 5 should be accounted for -// assertEquals(allUtxos.length, 5, "Should have accounted for all UTXOs"); -// }); - -// // Test withdrawing from an UNSPENT UTXO -// await t.step( -// "should withdraw from an UNSPENT UTXO and update its state", -// async () => { -// // Get an UNSPENT UTXO -// const unspentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); -// assertExists( -// unspentUtxos.length > 0, -// "Should have at least one UNSPENT UTXO" -// ); -// const testUtxo = unspentUtxos[0]; - -// // For finding the index, we know we've been working with index 0 in previous steps -// const utxoIndex = 0; - -// // Generate withdraw payload -// const withdrawPayload = poolEngine.buildWithdrawPayload({ -// utxo: testUtxo.publicKey, -// amount: depositAmount, -// }); - -// // Sign the payload -// const signature = await testUtxo.signPayload(withdrawPayload); -// assertExists(signature, "Should generate a valid signature"); - -// // Execute withdrawal -// const withdrawResult = (await poolEngine.write({ -// ...txInvocation, -// method: WriteMethods.withdraw, -// methodArgs: { -// to: account.getPublicKey(), -// amount: depositAmount, -// utxo: Buffer.from(testUtxo.publicKey), -// signature: Buffer.from(signature), -// }, -// options: { verboseOutput: true, includeHashOutput: true }, -// })) as SorobanTransactionPipelineOutputVerbose; - -// assertExists( -// withdrawResult.sorobanTransactionOutput, -// "Withdraw transaction result should exist" -// ); - -// // Manually update the UTXO state to SPENT after withdrawal -// utxoAccount.updateUTXOState(utxoIndex, UTXOStatus.SPENT); - -// // Refresh UTXO states -// await utxoAccount.batchLoad(); - -// // Verify the balance is now zero -// const balanceResult = await poolEngine.read({ -// ...txInvocation, -// method: ReadMethods.balance, -// methodArgs: { -// utxo: Buffer.from(testUtxo.publicKey), -// }, -// }); - -// assertEquals( -// balanceResult, -// 0n, -// "UTXO balance should be zero after withdrawal" -// ); - -// // Verify the UTXO is now in the SPENT collection -// const spentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.SPENT); -// const isTestUtxoSpent = spentUtxos.some( -// (spentUtxo) => -// Buffer.from(spentUtxo.publicKey).compare( -// Buffer.from(testUtxo.publicKey) -// ) === 0 -// ); - -// assertEquals( -// isTestUtxoSpent, -// true, -// "The withdrawn UTXO should be marked as SPENT" -// ); -// } -// ); - -// // Test deriving additional UTXOs after the initial batch -// await t.step("should derive additional UTXO batches", async () => { -// const initialCount = [ -// ...utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT), -// ...utxoAccount.getUTXOsByState(UTXOStatus.SPENT), -// ...utxoAccount.getUTXOsByState(UTXOStatus.FREE), -// ].length; - -// // Derive another batch starting from index 5 -// await utxoAccount.deriveBatch({ startIndex: 5, count: 3 }); - -// const newCount = [ -// ...utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT), -// ...utxoAccount.getUTXOsByState(UTXOStatus.SPENT), -// ...utxoAccount.getUTXOsByState(UTXOStatus.FREE), -// ].length; - -// assertEquals(newCount, initialCount + 3, "Should have added 3 new UTXOs"); - -// // New UTXOs should be in FREE state -// const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); -// assertEquals( -// freeUtxos.length >= 3, -// true, -// "Should have at least 3 FREE UTXOs" -// ); -// }); - -// // Test error cases -// await t.step("should handle invalid operations correctly", async () => { -// // Attempt to withdraw with invalid signature -// const freeUtxo = utxoAccount.getUTXOsByState(UTXOStatus.FREE)[0]; -// assertExists(freeUtxo, "Should have a free UTXO for testing"); - -// // Try withdrawal without deposit -// const invalidWithdrawPayload = poolEngine.buildWithdrawPayload({ -// utxo: freeUtxo.publicKey, -// amount: 10n, // <- different amount than the actual amount in the transaction -// }); - -// const signature = await freeUtxo.signPayload(invalidWithdrawPayload); - -// try { -// await poolEngine.write({ -// ...txInvocation, -// method: WriteMethods.withdraw, -// methodArgs: { -// to: account.getPublicKey(), -// amount: 100000n, -// utxo: Buffer.from(freeUtxo.publicKey), -// signature: Buffer.from(signature), -// }, -// }); -// throw new Error("Should have failed"); -// } catch (error) { -// assertExists(error, "Expected error for withdrawal without balance"); -// } - -// // Try zero amount deposit -// // todo: Review contract behavior for zero amount deposits - -// // try { -// // await poolEngine.write({ -// // ...txInvocation, -// // method: WriteMethods.deposit, -// // methodArgs: { -// // from: account.getPublicKey(), -// // amount: 0n, -// // utxo: Buffer.from(freeUtxo.publicKey), -// // }, -// // }); -// // throw new Error("Should have failed"); -// // } catch (error) { -// // assertExists(error, "Expected error for zero amount deposit"); -// // } -// }); - -// // Test UTXO reservation functionality -// await t.step("should handle UTXO reservations correctly", async () => { -// // Clear any existing reservations -// utxoAccount.releaseStaleReservations(0); - -// // Derive some UTXOs for testing -// await utxoAccount.deriveBatch({ count: 5 }); -// await utxoAccount.batchLoad(); // Update states - -// // Try reserving more UTXOs than available -// const freeCount = utxoAccount.getUTXOsByState(UTXOStatus.FREE).length; -// const tooManyReserved = utxoAccount.reserveUTXOs(freeCount + 5); -// assertEquals( -// tooManyReserved, -// null, -// "Should return null when requesting too many UTXOs" -// ); - -// // Reserve some UTXOs -// const reservedCount = 2; -// const reserved = utxoAccount.reserveUTXOs(reservedCount); -// assertExists(reserved, "Should successfully reserve UTXOs"); - -// assertEquals( -// reserved.length, -// reservedCount, -// "Should reserve exactly the requested number of UTXOs" -// ); - -// // Verify these UTXOs are actually reserved -// const reservedUTXOs = utxoAccount.getReservedUTXOs(); -// assertEquals( -// reservedUTXOs.length >= reservedCount, -// true, -// "Should track reserved UTXOs" -// ); - -// // Verify we can reserve more UTXOs -// const secondReservation = utxoAccount.reserveUTXOs(1); -// assertExists( -// secondReservation, -// "Should be able to reserve different UTXOs" -// ); -// }); - -// // Test UTXO selection strategies -// await t.step("should select UTXOs using different strategies", async () => { -// // Create some unspent UTXOs with different balances -// const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); - -// // Check if we have enough free UTXOs -// if (freeUtxos.length < 2) { -// // Derive more UTXOs if needed -// await utxoAccount.deriveBatch({ count: 5 }); -// } - -// const testUtxo1 = utxoAccount.getUTXOsByState(UTXOStatus.FREE)[0]; -// const testUtxo2 = utxoAccount.getUTXOsByState(UTXOStatus.FREE)[1]; -// assertExists(testUtxo1, "Should have a free UTXO for testing"); -// assertExists(testUtxo2, "Should have another free UTXO for testing"); - -// // Get indices for the test UTXOs -// const index1 = Number(testUtxo1.index); -// const index2 = Number(testUtxo2.index); - -// // Deposit different amounts -// await poolEngine.write({ -// ...txInvocation, -// method: WriteMethods.deposit, -// methodArgs: { -// from: account.getPublicKey(), -// amount: 1000000n, -// utxo: Buffer.from(testUtxo1.publicKey), -// }, -// }); - -// await poolEngine.write({ -// ...txInvocation, -// method: WriteMethods.deposit, -// methodArgs: { -// from: account.getPublicKey(), -// amount: 500000n, -// utxo: Buffer.from(testUtxo2.publicKey), -// }, -// }); - -// // Update UTXO states -// utxoAccount.updateUTXOState(index1, UTXOStatus.UNSPENT, 1000000n); -// utxoAccount.updateUTXOState(index2, UTXOStatus.UNSPENT, 500000n); - -// // Check balances using contract to validate our updates -// const balance1 = await poolEngine.read({ -// ...txInvocation, -// method: ReadMethods.balance, -// methodArgs: { -// utxo: Buffer.from(testUtxo1.publicKey), -// }, -// }); - -// // Skip the actual test if balance verification fails -// if (balance1 === 1000000n) { -// // Test sequential selection -// const sequentialResult = utxoAccount.selectUTXOsForTransfer(750000n); -// assertExists(sequentialResult, "Should find UTXOs for transfer"); -// assertEquals( -// sequentialResult.selectedUTXOs.length >= 1, -// true, -// "Should select at least one UTXO when possible" -// ); -// } -// }); - -// // Test stale reservation release -// await t.step("should release stale reservations", async () => { -// // Clear any existing reservations -// utxoAccount.releaseStaleReservations(0); - -// // Reserve some UTXOs -// const reserved = utxoAccount.reserveUTXOs(2); -// assertExists(reserved, "Should successfully reserve UTXOs"); - -// // Release stale reservations (all of them, since we're using a 0ms age) -// const releasedCount = utxoAccount.releaseStaleReservations(0); -// assertEquals(releasedCount, 2, "Should release all stale reservations"); - -// // Verify they can be reserved again -// const newReservation = utxoAccount.reserveUTXOs(2); -// assertExists( -// newReservation, -// "Should be able to reserve previously stale UTXOs" -// ); -// }); -// }); +// deno-lint-ignore-file require-await +import { assertEquals, assertExists } from "https://deno.land/std@0.207.0/assert/mod.ts"; +import { beforeAll, describe, it } from "https://deno.land/std@0.207.0/testing/bdd.ts"; + +import { + LocalSigner, + NativeAccount, + TestNet, + initializeWithFriendbot, + Contract, + P_SimulateTransactionErrors, + type Ed25519PublicKey, + type TransactionConfig, + type ContractId, + type TestNetConfig, + type Ed25519SecretKey, +} from "@colibri/core"; +import { + AuthInvokeMethods, + AuthSpec, +} from "../../src/channel-auth/constants.ts"; +import { Buffer } from "node:buffer"; +import { loadContractWasm } from "../helpers/load-wasm.ts"; +import type { ChannelAuthConstructorArgs } from "../../src/channel-auth/types.ts"; +import type { ChannelConstructorArgs } from "../../src/privacy-channel/types.ts"; +import { + ChannelInvokeMethods, + ChannelReadMethods, + ChannelSpec, +} from "../../src/privacy-channel/constants.ts"; +import { Asset, Keypair } from "@stellar/stellar-sdk"; +import { PrivacyChannel } from "../../src/privacy-channel/index.ts"; +import { UtxoBasedStellarAccount } from "../../src/utxo-based-account/utxo-based-stellar-account/index.ts"; +import { UTXOStatus } from "../../src/core/utxo-keypair/types.ts"; +import { StellarDerivator } from "../../src/derivation/stellar/index.ts"; +import { MoonlightTransactionBuilder } from "../../src/transaction-builder/index.ts"; +import { MoonlightOperation as op } from "../../src/operation/index.ts"; +import { generateNonce } from "../../src/utils/common/index.ts"; +import { Server } from "@stellar/stellar-sdk/rpc"; +import { disableSanitizeConfig } from "../utils/disable-sanitize-config.ts"; +import { StellarNetworkId } from "../../src/derivation/stellar/stellar-network-id.ts"; + +describe( + "[Testnet - Integration] UtxoBasedAccount", + disableSanitizeConfig, + () => { + const networkConfig: TestNetConfig = TestNet() as TestNetConfig; + + const admin = NativeAccount.fromMasterSigner(LocalSigner.generateRandom()); + const providerKeys = Keypair.random(); + const userKeys = Keypair.random(); + + const provider = NativeAccount.fromMasterSigner( + LocalSigner.fromSecret(providerKeys.secret() as Ed25519SecretKey) + ); + + const user = NativeAccount.fromMasterSigner( + LocalSigner.fromSecret(userKeys.secret() as Ed25519SecretKey) + ); + + const txConfig: TransactionConfig = { + fee: "1000000", + timeout: 60, + source: admin.address(), + signers: [admin.signer()], + }; + + const assetId = Asset.native().contractId( + networkConfig.networkPassphrase + ) as ContractId; + + let authWasm: Buffer; + let channelWasm: Buffer; + let authId: ContractId; + let channelId: ContractId; + let channelClient: PrivacyChannel; + let rpc: Server; + + beforeAll(async () => { + // Initialize accounts with friendbot + await initializeWithFriendbot( + networkConfig.friendbotUrl, + admin.address() as Ed25519PublicKey + ); + + await initializeWithFriendbot( + networkConfig.friendbotUrl, + provider.address() as Ed25519PublicKey + ); + + await initializeWithFriendbot( + networkConfig.friendbotUrl, + user.address() as Ed25519PublicKey + ); + + // Load contract WASMs + authWasm = loadContractWasm("channel_auth_contract"); + channelWasm = loadContractWasm("privacy_channel"); + + // Deploy ChannelAuth contract + const authContract = Contract.create({ + networkConfig, + contractConfig: { + spec: AuthSpec, + wasm: authWasm, + }, + }); + + await authContract.uploadWasm({ + ...txConfig, + }); + + await authContract.deploy({ + config: txConfig, + constructorArgs: { + admin: admin.address() as Ed25519PublicKey, + } as ChannelAuthConstructorArgs, + }); + + authId = authContract.getContractId(); + + // Add provider to auth contract + await authContract.invoke({ + method: AuthInvokeMethods.add_provider, + methodArgs: { + provider: provider.address() as Ed25519PublicKey, + }, + config: { ...txConfig, signers: [admin.signer(), provider.signer()] }, + }); + }); + + describe("Core Functionality", () => { + beforeAll(async () => { + // Initialize RPC server + rpc = new Server(networkConfig.rpcUrl as string); + + // Deploy PrivacyChannel contract + const channelContract = Contract.create({ + networkConfig, + contractConfig: { + spec: ChannelSpec, + wasm: channelWasm, + }, + }); + + await channelContract.uploadWasm({ + ...txConfig, + }); + + await channelContract.deploy({ + config: txConfig, + constructorArgs: { + admin: admin.address() as Ed25519PublicKey, + auth_contract: authId, + asset: assetId, + } as ChannelConstructorArgs, + }); + + channelId = channelContract.getContractId(); + + // Create PrivacyChannel client + channelClient = new PrivacyChannel( + networkConfig, + channelId, + authId, + assetId + ); + }); + + it("should initialize PrivacyChannel client", () => { + assertExists(channelClient); + assertExists(channelClient.getAuthId()); + assertEquals(channelClient.getAuthId(), authId); + assertExists(channelClient.getNetworkConfig()); + assertEquals(channelClient.getNetworkConfig(), networkConfig); + assertExists(channelClient.getAssetId()); + assertEquals(channelClient.getAssetId(), assetId); + assertExists(channelClient.getChannelId()); + assertEquals(channelClient.getChannelId(), channelId); + assertExists(channelClient.getDerivator()); + }); + + it("should initialize UtxoBasedStellarAccount", () => { + const testRoot = "S-TEST_SECRET_ROOT"; + + const utxoAccount = new UtxoBasedStellarAccount({ + derivator: channelClient.getDerivator(), + root: testRoot, + options: { + batchSize: 10, + fetchBalances: async (publicKeys: Uint8Array[]) => { + return channelClient.read({ + method: ChannelReadMethods.utxo_balances, + methodArgs: { + utxos: publicKeys.map((pk) => Buffer.from(pk)), + }, + }); + }, + }, + }); + + assertExists(utxoAccount); + assertEquals(utxoAccount.getNextIndex(), 1); + }); + + it("should derive a batch of UTXO keypairs", async () => { + const testRoot = "S-TEST_SECRET_ROOT_2"; + + // Create a fresh derivator for this test + const stelalrDerivator = new StellarDerivator().withNetworkAndContract( + StellarNetworkId.Testnet, + channelId + ); + + const utxoAccount = new UtxoBasedStellarAccount({ + derivator: stelalrDerivator, + root: testRoot, + options: { + batchSize: 10, + fetchBalances: async (publicKeys: Uint8Array[]) => { + return channelClient.read({ + method: ChannelReadMethods.utxo_balances, + methodArgs: { + utxos: publicKeys.map((pk) => Buffer.from(pk)), + }, + }); + }, + }, + }); + + const batchSize = 5; + await utxoAccount.deriveBatch({ startIndex: 0, count: batchSize }); + + const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); + assertEquals( + freeUtxos.length, + batchSize, + "Should have derived the correct number of UTXOs" + ); + + // Verify each UTXO has required properties + for (const utxo of freeUtxos) { + assertExists(utxo.publicKey, "UTXO should have a public key"); + assertExists(utxo.privateKey, "UTXO should have a private key"); + + // Verify the keypair can sign data + const testData = new Uint8Array(32); + crypto.getRandomValues(testData); + const signature = await utxo.signPayload(testData); + assertExists(signature, "Should be able to generate a signature"); + } + }); + + it("should deposit to UTXO and verify state transition to UNSPENT", async () => { + const testRoot = "S-TEST_SECRET_ROOT_3"; + const depositAmount = 500000n; // 0.05 XLM + + // Create a fresh derivator for this test + const freshDerivator = new StellarDerivator().withNetworkAndContract( + StellarNetworkId.Testnet, + channelId + ); + + const utxoAccount = new UtxoBasedStellarAccount({ + derivator: freshDerivator, + root: testRoot, + options: { + batchSize: 10, + fetchBalances: async (publicKeys: Uint8Array[]) => { + return channelClient.read({ + method: ChannelReadMethods.utxo_balances, + methodArgs: { + utxos: publicKeys.map((pk) => Buffer.from(pk)), + }, + }); + }, + }, + }); + + // Derive UTXOs + await utxoAccount.deriveBatch({ startIndex: 0, count: 1 }); + const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); + assertEquals(freeUtxos.length, 1, "Should have one FREE UTXO"); + + const testUtxo = freeUtxos[0]; + assertExists(testUtxo, "Should have a test UTXO"); + + // Build deposit transaction + const depositTx = new MoonlightTransactionBuilder({ + network: networkConfig.networkPassphrase, + channelId: channelId, + authId: authId, + asset: Asset.native(), + }); + + const createOp = op.create(testUtxo.publicKey, depositAmount); + depositTx.addOperation(createOp); + depositTx.addOperation( + op + .deposit(user.address() as Ed25519PublicKey, depositAmount) + .addConditions([createOp.toCondition()]) + ); + + // Get latest ledger for signature expiration + const latestLedger = await rpc.getLatestLedger(); + const signatureExpirationLedger = latestLedger.sequence + 100; + const nonce = generateNonce(); + + // Sign the transaction + await depositTx.signExtWithEd25519( + userKeys, + signatureExpirationLedger, + nonce + ); + + await depositTx.signWithProvider( + providerKeys, + signatureExpirationLedger, + nonce + ); + + // Execute the deposit transaction + await channelClient + .invokeRaw({ + operationArgs: { + function: ChannelInvokeMethods.transact, + args: [depositTx.buildXDR()], + auth: [...depositTx.getSignedAuthEntries()], + }, + config: txConfig, + }) + .catch((e) => { + if (e instanceof P_SimulateTransactionErrors.SIMULATION_FAILED) { + console.error("Error invoking contract:", e); + console.error( + "Transaction XDR:", + e.meta.data.input.transaction.toXDR() + ); + } + throw e; + }); + + // Update UTXO state manually (simulating what batchLoad would do) + utxoAccount.updateUTXOState(0, UTXOStatus.UNSPENT, depositAmount); + + // Verify the balance through the contract + const balanceResult = await channelClient.read({ + method: ChannelReadMethods.utxo_balance, + methodArgs: { + utxo: Buffer.from(testUtxo.publicKey), + }, + }); + + assertEquals( + balanceResult, + depositAmount, + "UTXO balance should match the deposited amount" + ); + + // Verify the UTXO state changed to UNSPENT + const unspentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); + assertEquals(unspentUtxos.length, 1, "Should have one UNSPENT UTXO"); + + const unspentUtxo = unspentUtxos[0]; + assertEquals(unspentUtxo.balance, depositAmount, "UTXO should have correct balance"); + }); + + it("should calculate total balance across UNSPENT UTXOs", async () => { + const testRoot = "S-TEST_SECRET_ROOT_4"; + + // Create a fresh derivator for this test + const stellarDerivator = new StellarDerivator().withNetworkAndContract( + StellarNetworkId.Testnet, + channelId + ); + + const utxoAccount = new UtxoBasedStellarAccount({ + derivator: stellarDerivator, + root: testRoot, + options: { + batchSize: 10, + fetchBalances: async (publicKeys: Uint8Array[]) => { + return channelClient.read({ + method: ChannelReadMethods.utxo_balances, + methodArgs: { + utxos: publicKeys.map((pk) => Buffer.from(pk)), + }, + }); + }, + }, + }); + + // Derive 3 UTXOs + await utxoAccount.deriveBatch({ startIndex: 0, count: 3 }); + const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); + assertEquals(freeUtxos.length, 3, "Should have 3 FREE UTXOs"); + + const amounts = [100000n, 200000n, 300000n]; // 0.01, 0.02, 0.03 XLM + const expectedTotal = 600000n; + + // Deposit different amounts to each UTXO + for (let i = 0; i < 3; i++) { + const testUtxo = freeUtxos[i]; + const amount = amounts[i]; + + const depositTx = new MoonlightTransactionBuilder({ + network: networkConfig.networkPassphrase, + channelId: channelId, + authId: authId, + asset: Asset.native(), + }); + + const createOp = op.create(testUtxo.publicKey, amount); + depositTx.addOperation(createOp); + depositTx.addOperation( + op + .deposit(user.address() as Ed25519PublicKey, amount) + .addConditions([createOp.toCondition()]) + ); + + const latestLedger = await rpc.getLatestLedger(); + const signatureExpirationLedger = latestLedger.sequence + 100; + const nonce = generateNonce(); + + await depositTx.signExtWithEd25519( + userKeys, + signatureExpirationLedger, + nonce + ); + + await depositTx.signWithProvider( + providerKeys, + signatureExpirationLedger, + nonce + ); + + await channelClient.invokeRaw({ + operationArgs: { + function: ChannelInvokeMethods.transact, + args: [depositTx.buildXDR()], + auth: [...depositTx.getSignedAuthEntries()], + }, + config: txConfig, + }); + + // Update UTXO state to UNSPENT + utxoAccount.updateUTXOState(i, UTXOStatus.UNSPENT, amount); + } + + // Verify all UTXOs are UNSPENT + const unspentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); + assertEquals(unspentUtxos.length, 3, "Should have 3 UNSPENT UTXOs"); + + // Verify individual balances + for (let i = 0; i < 3; i++) { + assertEquals( + unspentUtxos[i].balance, + amounts[i], + `UTXO ${i} should have correct balance` + ); + } + + // Calculate and verify total balance + const totalBalance = utxoAccount.getTotalBalance(); + assertEquals( + totalBalance, + expectedTotal, + "Total balance should be sum of all UNSPENT UTXOs" + ); + }); + + it("should withdraw from UNSPENT UTXO and verify state to SPENT", async () => { + const testRoot = "S-TEST_SECRET_ROOT_5"; + const depositAmount = 500000n; // 0.05 XLM + + // Create a fresh derivator for this test + const stellarDerivator = new StellarDerivator().withNetworkAndContract( + StellarNetworkId.Testnet, + channelId + ); + + const utxoAccount = new UtxoBasedStellarAccount({ + derivator: stellarDerivator, + root: testRoot, + options: { + batchSize: 10, + fetchBalances: async (publicKeys: Uint8Array[]) => { + return channelClient.read({ + method: ChannelReadMethods.utxo_balances, + methodArgs: { + utxos: publicKeys.map((pk) => Buffer.from(pk)), + }, + }); + }, + }, + }); + + // Derive UTXOs + await utxoAccount.deriveBatch({ startIndex: 0, count: 1 }); + const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); + assertEquals(freeUtxos.length, 1, "Should have one FREE UTXO"); + + const testUtxo = freeUtxos[0]; + + // First deposit to the UTXO + const depositTx = new MoonlightTransactionBuilder({ + network: networkConfig.networkPassphrase, + channelId: channelId, + authId: authId, + asset: Asset.native(), + }); + + const createOp = op.create(testUtxo.publicKey, depositAmount); + depositTx.addOperation(createOp); + depositTx.addOperation( + op + .deposit(user.address() as Ed25519PublicKey, depositAmount) + .addConditions([createOp.toCondition()]) + ); + + const latestLedger = await rpc.getLatestLedger(); + const signatureExpirationLedger = latestLedger.sequence + 100; + const nonce = generateNonce(); + + await depositTx.signExtWithEd25519( + userKeys, + signatureExpirationLedger, + nonce + ); + + await depositTx.signWithProvider( + providerKeys, + signatureExpirationLedger, + nonce + ); + + await channelClient.invokeRaw({ + operationArgs: { + function: ChannelInvokeMethods.transact, + args: [depositTx.buildXDR()], + auth: [...depositTx.getSignedAuthEntries()], + }, + config: txConfig, + }); + + // Update UTXO state to UNSPENT + utxoAccount.updateUTXOState(0, UTXOStatus.UNSPENT, depositAmount); + + // Verify deposit worked + const unspentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); + assertEquals(unspentUtxos.length, 1, "Should have one UNSPENT UTXO"); + + // Verify UTXO has balance before withdraw + const balanceBeforeWithdraw = await channelClient.read({ + method: ChannelReadMethods.utxo_balance, + methodArgs: { + utxo: Buffer.from(testUtxo.publicKey), + }, + }); + assertEquals(balanceBeforeWithdraw, depositAmount, "UTXO should have balance before withdraw"); + + // Now withdraw from the UTXO + const withdrawTx = new MoonlightTransactionBuilder({ + network: networkConfig.networkPassphrase, + channelId: channelId, + authId: authId, + asset: Asset.native(), + }); + + const withdrawOp = op.withdraw(user.address(), depositAmount); + + const spendOp = op.spend(testUtxo.publicKey); + spendOp.addCondition(withdrawOp.toCondition()); + + withdrawTx.addOperation(spendOp).addOperation(withdrawOp); + + // Sign with the UTXO keypair + await withdrawTx.signWithSpendUtxo( + testUtxo, + signatureExpirationLedger + ); + + await withdrawTx.signWithProvider( + providerKeys, + signatureExpirationLedger, + generateNonce() + ); + + // Execute the withdraw transaction + await channelClient.invokeRaw({ + operationArgs: { + function: ChannelInvokeMethods.transact, + args: [withdrawTx.buildXDR()], + auth: [...withdrawTx.getSignedAuthEntries()], + }, + config: txConfig, + }).catch((e) => { + if (e instanceof P_SimulateTransactionErrors.SIMULATION_FAILED) { + console.error("Error invoking withdraw contract:", e); + console.error( + "Transaction XDR:", + e.meta.data.input.transaction.toXDR() + ); + } + throw e; + }); + + // Update UTXO state to SPENT + utxoAccount.updateUTXOState(0, UTXOStatus.SPENT, 0n); + + // Verify the balance through the contract is 0 + const balanceResult = await channelClient.read({ + method: ChannelReadMethods.utxo_balance, + methodArgs: { + utxo: Buffer.from(testUtxo.publicKey), + }, + }); + + assertEquals( + balanceResult, + 0n, + "UTXO balance should be 0 after withdrawal" + ); + + // Verify the UTXO state changed to SPENT + const spentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.SPENT); + assertEquals(spentUtxos.length, 1, "Should have one SPENT UTXO"); + + const spentUtxo = spentUtxos[0]; + assertEquals(spentUtxo.balance, 0n, "SPENT UTXO should have 0 balance"); + }); + + it("should batch load UTXO balances from contract", async () => { + const testRoot = "S-TEST_SECRET_ROOT_6"; + + // Create a fresh derivator for this test + const stellarDerivator = new StellarDerivator().withNetworkAndContract( + StellarNetworkId.Testnet, + channelId + ); + + const utxoAccount = new UtxoBasedStellarAccount({ + derivator: stellarDerivator, + root: testRoot, + options: { + batchSize: 10, + fetchBalances: async (publicKeys: Uint8Array[]) => { + return channelClient.read({ + method: ChannelReadMethods.utxo_balances, + methodArgs: { + utxos: publicKeys.map((pk) => Buffer.from(pk)), + }, + }); + }, + }, + }); + + // Derive 5 UTXOs + await utxoAccount.deriveBatch({ startIndex: 0, count: 5 }); + const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); + assertEquals(freeUtxos.length, 5, "Should have 5 FREE UTXOs"); + + const amounts = [100000n, 200000n]; // Deposit to 2 UTXOs only + + // Deposit to first 2 UTXOs + for (let i = 0; i < 2; i++) { + const testUtxo = freeUtxos[i]; + const amount = amounts[i]; + + const depositTx = new MoonlightTransactionBuilder({ + network: networkConfig.networkPassphrase, + channelId: channelId, + authId: authId, + asset: Asset.native(), + }); + + const createOp = op.create(testUtxo.publicKey, amount); + depositTx.addOperation(createOp); + depositTx.addOperation( + op + .deposit(user.address() as Ed25519PublicKey, amount) + .addConditions([createOp.toCondition()]) + ); + + const latestLedger = await rpc.getLatestLedger(); + const signatureExpirationLedger = latestLedger.sequence + 100; + const nonce = generateNonce(); + + await depositTx.signExtWithEd25519( + userKeys, + signatureExpirationLedger, + nonce + ); + + await depositTx.signWithProvider( + providerKeys, + signatureExpirationLedger, + nonce + ); + + await channelClient.invokeRaw({ + operationArgs: { + function: ChannelInvokeMethods.transact, + args: [depositTx.buildXDR()], + auth: [...depositTx.getSignedAuthEntries()], + }, + config: txConfig, + }); + } + + // Now call batchLoad to update states from contract + await utxoAccount.batchLoad(); + + // Verify that 2 UTXOs are UNSPENT (have balances) + const unspentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); + assertEquals(unspentUtxos.length, 2, "Should have 2 UNSPENT UTXOs after batchLoad"); + + // Verify that 3 UTXOs are still FREE (no balances) + const freeUtxosAfterLoad = utxoAccount.getUTXOsByState(UTXOStatus.FREE); + assertEquals(freeUtxosAfterLoad.length, 3, "Should have 3 FREE UTXOs after batchLoad"); + + // Verify the balances are correct + for (let i = 0; i < 2; i++) { + assertEquals( + unspentUtxos[i].balance, + amounts[i], + `UTXO ${i} should have correct balance after batchLoad` + ); + } + + // Verify total balance + const totalBalance = utxoAccount.getTotalBalance(); + const expectedTotal = amounts[0] + amounts[1]; + assertEquals( + totalBalance, + expectedTotal, + "Total balance should be sum of UNSPENT UTXOs" + ); + }); + }); + + describe("Advanced Features", () => { + it("should handle multiple deposits and withdrawals across different UTXOs", async () => { + const testRoot = "S-TEST_SECRET_ROOT_7"; + + // Create a fresh derivator for this test + const stellarDerivator = new StellarDerivator().withNetworkAndContract( + StellarNetworkId.Testnet, + channelId + ); + + const utxoAccount = new UtxoBasedStellarAccount({ + derivator: stellarDerivator, + root: testRoot, + options: { + batchSize: 10, + fetchBalances: async (publicKeys: Uint8Array[]) => { + return channelClient.read({ + method: ChannelReadMethods.utxo_balances, + methodArgs: { + utxos: publicKeys.map((pk) => Buffer.from(pk)), + }, + }); + }, + }, + }); + + // Step 1: Derive 5 UTXOs + await utxoAccount.deriveBatch({ startIndex: 0, count: 5 }); + const allUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); + assertEquals(allUtxos.length, 5, "Should have 5 FREE UTXOs"); + + const depositAmounts = [100000n, 200000n, 300000n]; // 0.01, 0.02, 0.03 XLM + + // Step 2: Deposit to first 3 UTXOs + for (let i = 0; i < 3; i++) { + const testUtxo = allUtxos[i]; + const amount = depositAmounts[i]; + + const depositTx = new MoonlightTransactionBuilder({ + network: networkConfig.networkPassphrase, + channelId: channelId, + authId: authId, + asset: Asset.native(), + }); + + const createOp = op.create(testUtxo.publicKey, amount); + depositTx.addOperation(createOp); + depositTx.addOperation( + op + .deposit(user.address() as Ed25519PublicKey, amount) + .addConditions([createOp.toCondition()]) + ); + + const latestLedger = await rpc.getLatestLedger(); + const signatureExpirationLedger = latestLedger.sequence + 100; + const nonce = generateNonce(); + + await depositTx.signExtWithEd25519( + userKeys, + signatureExpirationLedger, + nonce + ); + + await depositTx.signWithProvider( + providerKeys, + signatureExpirationLedger, + nonce + ); + + await channelClient.invokeRaw({ + operationArgs: { + function: ChannelInvokeMethods.transact, + args: [depositTx.buildXDR()], + auth: [...depositTx.getSignedAuthEntries()], + }, + config: txConfig, + }); + + utxoAccount.updateUTXOState(i, UTXOStatus.UNSPENT, amount); + } + + // Step 3: Verify states after deposits (3 UNSPENT, 2 FREE) + const unspentAfterDeposits = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); + const freeAfterDeposits = utxoAccount.getUTXOsByState(UTXOStatus.FREE); + assertEquals(unspentAfterDeposits.length, 3, "Should have 3 UNSPENT UTXOs after deposits"); + assertEquals(freeAfterDeposits.length, 2, "Should have 2 FREE UTXOs after deposits"); + + const totalAfterDeposits = utxoAccount.getTotalBalance(); + assertEquals(totalAfterDeposits, 600000n, "Total balance should be 600000 after deposits"); + + // Step 4: Withdraw from first 2 UTXOs + for (let i = 0; i < 2; i++) { + const testUtxo = allUtxos[i]; + const amount = depositAmounts[i]; + + const withdrawTx = new MoonlightTransactionBuilder({ + network: networkConfig.networkPassphrase, + channelId: channelId, + authId: authId, + asset: Asset.native(), + }); + + const withdrawOp = op.withdraw(user.address(), amount); + const spendOp = op.spend(testUtxo.publicKey); + spendOp.addCondition(withdrawOp.toCondition()); + + withdrawTx.addOperation(spendOp).addOperation(withdrawOp); + + const latestLedger = await rpc.getLatestLedger(); + const signatureExpirationLedger = latestLedger.sequence + 100; + + await withdrawTx.signWithSpendUtxo( + testUtxo, + signatureExpirationLedger + ); + + await withdrawTx.signWithProvider( + providerKeys, + signatureExpirationLedger, + generateNonce() + ); + + await channelClient.invokeRaw({ + operationArgs: { + function: ChannelInvokeMethods.transact, + args: [withdrawTx.buildXDR()], + auth: [...withdrawTx.getSignedAuthEntries()], + }, + config: txConfig, + }); + + utxoAccount.updateUTXOState(i, UTXOStatus.SPENT, 0n); + } + + // Step 5: Verify states after withdraws (1 UNSPENT, 2 SPENT, 2 FREE) + const unspentAfterWithdraws = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); + const spentAfterWithdraws = utxoAccount.getUTXOsByState(UTXOStatus.SPENT); + const freeAfterWithdraws = utxoAccount.getUTXOsByState(UTXOStatus.FREE); + + assertEquals(unspentAfterWithdraws.length, 1, "Should have 1 UNSPENT UTXO after withdraws"); + assertEquals(spentAfterWithdraws.length, 2, "Should have 2 SPENT UTXOs after withdraws"); + assertEquals(freeAfterWithdraws.length, 2, "Should have 2 FREE UTXOs after withdraws"); + + const totalAfterWithdraws = utxoAccount.getTotalBalance(); + assertEquals(totalAfterWithdraws, 300000n, "Total balance should be 300000 after withdraws (only third UTXO)"); + + // Step 6: Make new deposit to one of the FREE UTXOs + const freeUtxo = freeAfterWithdraws[0]; + const newDepositAmount = 150000n; // 0.015 XLM + + const newDepositTx = new MoonlightTransactionBuilder({ + network: networkConfig.networkPassphrase, + channelId: channelId, + authId: authId, + asset: Asset.native(), + }); + + const createOp = op.create(freeUtxo.publicKey, newDepositAmount); + newDepositTx.addOperation(createOp); + newDepositTx.addOperation( + op + .deposit(user.address() as Ed25519PublicKey, newDepositAmount) + .addConditions([createOp.toCondition()]) + ); + + const latestLedger = await rpc.getLatestLedger(); + const signatureExpirationLedger = latestLedger.sequence + 100; + const nonce = generateNonce(); + + await newDepositTx.signExtWithEd25519( + userKeys, + signatureExpirationLedger, + nonce + ); + + await newDepositTx.signWithProvider( + providerKeys, + signatureExpirationLedger, + nonce + ); + + await channelClient.invokeRaw({ + operationArgs: { + function: ChannelInvokeMethods.transact, + args: [newDepositTx.buildXDR()], + auth: [...newDepositTx.getSignedAuthEntries()], + }, + config: txConfig, + }); + + utxoAccount.updateUTXOState(3, UTXOStatus.UNSPENT, newDepositAmount); + + // Step 7: Verify final state + const finalUnspent = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); + const finalSpent = utxoAccount.getUTXOsByState(UTXOStatus.SPENT); + const finalFree = utxoAccount.getUTXOsByState(UTXOStatus.FREE); + + assertEquals(finalUnspent.length, 2, "Should have 2 UNSPENT UTXOs at end"); + assertEquals(finalSpent.length, 2, "Should have 2 SPENT UTXOs at end"); + assertEquals(finalFree.length, 1, "Should have 1 FREE UTXO at end"); + + const finalTotal = utxoAccount.getTotalBalance(); + const expectedFinalTotal = 300000n + 150000n; // Third original + new deposit + assertEquals( + finalTotal, + expectedFinalTotal, + "Final balance should be sum of remaining UNSPENT UTXOs" + ); + + // Verify individual balances + assertEquals(finalUnspent[0].balance, 300000n, "First UNSPENT should have 300000"); + assertEquals(finalUnspent[1].balance, 150000n, "Second UNSPENT should have 150000"); + }); + }); + } +); \ No newline at end of file From 9cbfd7b9859cbbf2c533f0036585a6a8413956e5 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 23 Oct 2025 08:26:27 -0300 Subject: [PATCH 59/90] chore: update version to 0.3.0 in deno.json for release --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index a0c09c0..a9e9604 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@moonlight/moonlight-sdk", - "version": "0.2.1", + "version": "0.3.0", "description": "A privacy-focused toolkit for the Moonlight protocol on Stellar Soroban smart contracts.", "license": "MIT", "tasks": { From 3cc42567d6d3209922142dbde5c921a242012ee2 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 23 Oct 2025 08:32:32 -0300 Subject: [PATCH 60/90] delete: remove unused integration test file main.integration.tes.ts --- test/integration/main.integration.tes.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test/integration/main.integration.tes.ts diff --git a/test/integration/main.integration.tes.ts b/test/integration/main.integration.tes.ts deleted file mode 100644 index e69de29..0000000 From 344634ef37fa1702357242f6fcfe9d0bfdceb902 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 23 Oct 2025 08:32:40 -0300 Subject: [PATCH 61/90] refactor(tests): update import paths and clean up assertions in utxo-based-account integration tests - Changed import statements to use the new module paths from "@std/assert" and "@std/testing/bdd". - Removed unnecessary blank lines for improved readability. - Enhanced assertion formatting for better clarity in test cases. --- .../utxo-based-account.integration.test.ts | 179 ++++++++++++------ 1 file changed, 120 insertions(+), 59 deletions(-) diff --git a/test/integration/utxo-based-account.integration.test.ts b/test/integration/utxo-based-account.integration.test.ts index 881d496..eab14b3 100644 --- a/test/integration/utxo-based-account.integration.test.ts +++ b/test/integration/utxo-based-account.integration.test.ts @@ -1,6 +1,6 @@ // deno-lint-ignore-file require-await -import { assertEquals, assertExists } from "https://deno.land/std@0.207.0/assert/mod.ts"; -import { beforeAll, describe, it } from "https://deno.land/std@0.207.0/testing/bdd.ts"; +import { assertEquals, assertExists } from "@std/assert"; +import { beforeAll, describe, it } from "@std/testing/bdd"; import { LocalSigner, @@ -182,7 +182,7 @@ describe( it("should initialize UtxoBasedStellarAccount", () => { const testRoot = "S-TEST_SECRET_ROOT"; - + const utxoAccount = new UtxoBasedStellarAccount({ derivator: channelClient.getDerivator(), root: testRoot, @@ -205,13 +205,13 @@ describe( it("should derive a batch of UTXO keypairs", async () => { const testRoot = "S-TEST_SECRET_ROOT_2"; - + // Create a fresh derivator for this test const stelalrDerivator = new StellarDerivator().withNetworkAndContract( StellarNetworkId.Testnet, channelId ); - + const utxoAccount = new UtxoBasedStellarAccount({ derivator: stelalrDerivator, root: testRoot, @@ -254,13 +254,13 @@ describe( it("should deposit to UTXO and verify state transition to UNSPENT", async () => { const testRoot = "S-TEST_SECRET_ROOT_3"; const depositAmount = 500000n; // 0.05 XLM - + // Create a fresh derivator for this test const freshDerivator = new StellarDerivator().withNetworkAndContract( StellarNetworkId.Testnet, channelId ); - + const utxoAccount = new UtxoBasedStellarAccount({ derivator: freshDerivator, root: testRoot, @@ -281,7 +281,7 @@ describe( await utxoAccount.deriveBatch({ startIndex: 0, count: 1 }); const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); assertEquals(freeUtxos.length, 1, "Should have one FREE UTXO"); - + const testUtxo = freeUtxos[0]; assertExists(testUtxo, "Should have a test UTXO"); @@ -360,20 +360,24 @@ describe( // Verify the UTXO state changed to UNSPENT const unspentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); assertEquals(unspentUtxos.length, 1, "Should have one UNSPENT UTXO"); - + const unspentUtxo = unspentUtxos[0]; - assertEquals(unspentUtxo.balance, depositAmount, "UTXO should have correct balance"); + assertEquals( + unspentUtxo.balance, + depositAmount, + "UTXO should have correct balance" + ); }); it("should calculate total balance across UNSPENT UTXOs", async () => { const testRoot = "S-TEST_SECRET_ROOT_4"; - + // Create a fresh derivator for this test const stellarDerivator = new StellarDerivator().withNetworkAndContract( StellarNetworkId.Testnet, channelId ); - + const utxoAccount = new UtxoBasedStellarAccount({ derivator: stellarDerivator, root: testRoot, @@ -472,13 +476,13 @@ describe( it("should withdraw from UNSPENT UTXO and verify state to SPENT", async () => { const testRoot = "S-TEST_SECRET_ROOT_5"; const depositAmount = 500000n; // 0.05 XLM - + // Create a fresh derivator for this test const stellarDerivator = new StellarDerivator().withNetworkAndContract( StellarNetworkId.Testnet, channelId ); - + const utxoAccount = new UtxoBasedStellarAccount({ derivator: stellarDerivator, root: testRoot, @@ -499,7 +503,7 @@ describe( await utxoAccount.deriveBatch({ startIndex: 0, count: 1 }); const freeUtxos = utxoAccount.getUTXOsByState(UTXOStatus.FREE); assertEquals(freeUtxos.length, 1, "Should have one FREE UTXO"); - + const testUtxo = freeUtxos[0]; // First deposit to the UTXO @@ -557,7 +561,11 @@ describe( utxo: Buffer.from(testUtxo.publicKey), }, }); - assertEquals(balanceBeforeWithdraw, depositAmount, "UTXO should have balance before withdraw"); + assertEquals( + balanceBeforeWithdraw, + depositAmount, + "UTXO should have balance before withdraw" + ); // Now withdraw from the UTXO const withdrawTx = new MoonlightTransactionBuilder({ @@ -575,10 +583,7 @@ describe( withdrawTx.addOperation(spendOp).addOperation(withdrawOp); // Sign with the UTXO keypair - await withdrawTx.signWithSpendUtxo( - testUtxo, - signatureExpirationLedger - ); + await withdrawTx.signWithSpendUtxo(testUtxo, signatureExpirationLedger); await withdrawTx.signWithProvider( providerKeys, @@ -587,23 +592,25 @@ describe( ); // Execute the withdraw transaction - await channelClient.invokeRaw({ - operationArgs: { - function: ChannelInvokeMethods.transact, - args: [withdrawTx.buildXDR()], - auth: [...withdrawTx.getSignedAuthEntries()], - }, - config: txConfig, - }).catch((e) => { - if (e instanceof P_SimulateTransactionErrors.SIMULATION_FAILED) { - console.error("Error invoking withdraw contract:", e); - console.error( - "Transaction XDR:", - e.meta.data.input.transaction.toXDR() - ); - } - throw e; - }); + await channelClient + .invokeRaw({ + operationArgs: { + function: ChannelInvokeMethods.transact, + args: [withdrawTx.buildXDR()], + auth: [...withdrawTx.getSignedAuthEntries()], + }, + config: txConfig, + }) + .catch((e) => { + if (e instanceof P_SimulateTransactionErrors.SIMULATION_FAILED) { + console.error("Error invoking withdraw contract:", e); + console.error( + "Transaction XDR:", + e.meta.data.input.transaction.toXDR() + ); + } + throw e; + }); // Update UTXO state to SPENT utxoAccount.updateUTXOState(0, UTXOStatus.SPENT, 0n); @@ -625,20 +632,20 @@ describe( // Verify the UTXO state changed to SPENT const spentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.SPENT); assertEquals(spentUtxos.length, 1, "Should have one SPENT UTXO"); - + const spentUtxo = spentUtxos[0]; assertEquals(spentUtxo.balance, 0n, "SPENT UTXO should have 0 balance"); }); it("should batch load UTXO balances from contract", async () => { const testRoot = "S-TEST_SECRET_ROOT_6"; - + // Create a fresh derivator for this test const stellarDerivator = new StellarDerivator().withNetworkAndContract( StellarNetworkId.Testnet, channelId ); - + const utxoAccount = new UtxoBasedStellarAccount({ derivator: stellarDerivator, root: testRoot, @@ -713,11 +720,19 @@ describe( // Verify that 2 UTXOs are UNSPENT (have balances) const unspentUtxos = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); - assertEquals(unspentUtxos.length, 2, "Should have 2 UNSPENT UTXOs after batchLoad"); + assertEquals( + unspentUtxos.length, + 2, + "Should have 2 UNSPENT UTXOs after batchLoad" + ); // Verify that 3 UTXOs are still FREE (no balances) const freeUtxosAfterLoad = utxoAccount.getUTXOsByState(UTXOStatus.FREE); - assertEquals(freeUtxosAfterLoad.length, 3, "Should have 3 FREE UTXOs after batchLoad"); + assertEquals( + freeUtxosAfterLoad.length, + 3, + "Should have 3 FREE UTXOs after batchLoad" + ); // Verify the balances are correct for (let i = 0; i < 2; i++) { @@ -742,13 +757,13 @@ describe( describe("Advanced Features", () => { it("should handle multiple deposits and withdrawals across different UTXOs", async () => { const testRoot = "S-TEST_SECRET_ROOT_7"; - + // Create a fresh derivator for this test const stellarDerivator = new StellarDerivator().withNetworkAndContract( StellarNetworkId.Testnet, channelId ); - + const utxoAccount = new UtxoBasedStellarAccount({ derivator: stellarDerivator, root: testRoot, @@ -821,13 +836,27 @@ describe( } // Step 3: Verify states after deposits (3 UNSPENT, 2 FREE) - const unspentAfterDeposits = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); + const unspentAfterDeposits = utxoAccount.getUTXOsByState( + UTXOStatus.UNSPENT + ); const freeAfterDeposits = utxoAccount.getUTXOsByState(UTXOStatus.FREE); - assertEquals(unspentAfterDeposits.length, 3, "Should have 3 UNSPENT UTXOs after deposits"); - assertEquals(freeAfterDeposits.length, 2, "Should have 2 FREE UTXOs after deposits"); + assertEquals( + unspentAfterDeposits.length, + 3, + "Should have 3 UNSPENT UTXOs after deposits" + ); + assertEquals( + freeAfterDeposits.length, + 2, + "Should have 2 FREE UTXOs after deposits" + ); const totalAfterDeposits = utxoAccount.getTotalBalance(); - assertEquals(totalAfterDeposits, 600000n, "Total balance should be 600000 after deposits"); + assertEquals( + totalAfterDeposits, + 600000n, + "Total balance should be 600000 after deposits" + ); // Step 4: Withdraw from first 2 UTXOs for (let i = 0; i < 2; i++) { @@ -874,16 +903,36 @@ describe( } // Step 5: Verify states after withdraws (1 UNSPENT, 2 SPENT, 2 FREE) - const unspentAfterWithdraws = utxoAccount.getUTXOsByState(UTXOStatus.UNSPENT); - const spentAfterWithdraws = utxoAccount.getUTXOsByState(UTXOStatus.SPENT); + const unspentAfterWithdraws = utxoAccount.getUTXOsByState( + UTXOStatus.UNSPENT + ); + const spentAfterWithdraws = utxoAccount.getUTXOsByState( + UTXOStatus.SPENT + ); const freeAfterWithdraws = utxoAccount.getUTXOsByState(UTXOStatus.FREE); - - assertEquals(unspentAfterWithdraws.length, 1, "Should have 1 UNSPENT UTXO after withdraws"); - assertEquals(spentAfterWithdraws.length, 2, "Should have 2 SPENT UTXOs after withdraws"); - assertEquals(freeAfterWithdraws.length, 2, "Should have 2 FREE UTXOs after withdraws"); + + assertEquals( + unspentAfterWithdraws.length, + 1, + "Should have 1 UNSPENT UTXO after withdraws" + ); + assertEquals( + spentAfterWithdraws.length, + 2, + "Should have 2 SPENT UTXOs after withdraws" + ); + assertEquals( + freeAfterWithdraws.length, + 2, + "Should have 2 FREE UTXOs after withdraws" + ); const totalAfterWithdraws = utxoAccount.getTotalBalance(); - assertEquals(totalAfterWithdraws, 300000n, "Total balance should be 300000 after withdraws (only third UTXO)"); + assertEquals( + totalAfterWithdraws, + 300000n, + "Total balance should be 300000 after withdraws (only third UTXO)" + ); // Step 6: Make new deposit to one of the FREE UTXOs const freeUtxo = freeAfterWithdraws[0]; @@ -936,7 +985,11 @@ describe( const finalSpent = utxoAccount.getUTXOsByState(UTXOStatus.SPENT); const finalFree = utxoAccount.getUTXOsByState(UTXOStatus.FREE); - assertEquals(finalUnspent.length, 2, "Should have 2 UNSPENT UTXOs at end"); + assertEquals( + finalUnspent.length, + 2, + "Should have 2 UNSPENT UTXOs at end" + ); assertEquals(finalSpent.length, 2, "Should have 2 SPENT UTXOs at end"); assertEquals(finalFree.length, 1, "Should have 1 FREE UTXO at end"); @@ -949,9 +1002,17 @@ describe( ); // Verify individual balances - assertEquals(finalUnspent[0].balance, 300000n, "First UNSPENT should have 300000"); - assertEquals(finalUnspent[1].balance, 150000n, "Second UNSPENT should have 150000"); + assertEquals( + finalUnspent[0].balance, + 300000n, + "First UNSPENT should have 300000" + ); + assertEquals( + finalUnspent[1].balance, + 150000n, + "Second UNSPENT should have 150000" + ); }); }); } -); \ No newline at end of file +); From 61008d0a49220b2af19e0a8813b5574b8a11fd89 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 23 Oct 2025 08:33:35 -0300 Subject: [PATCH 62/90] fix(tests): correct variable name from 'stelalrDerivator' to 'stellarDerivator' in UtxoBasedAccount integration test for clarity and consistency --- test/integration/utxo-based-account.integration.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/utxo-based-account.integration.test.ts b/test/integration/utxo-based-account.integration.test.ts index eab14b3..2e19f52 100644 --- a/test/integration/utxo-based-account.integration.test.ts +++ b/test/integration/utxo-based-account.integration.test.ts @@ -207,13 +207,13 @@ describe( const testRoot = "S-TEST_SECRET_ROOT_2"; // Create a fresh derivator for this test - const stelalrDerivator = new StellarDerivator().withNetworkAndContract( + const stellarDerivator = new StellarDerivator().withNetworkAndContract( StellarNetworkId.Testnet, channelId ); const utxoAccount = new UtxoBasedStellarAccount({ - derivator: stelalrDerivator, + derivator: stellarDerivator, root: testRoot, options: { batchSize: 10, From 22a9b277f176cbd6971297108181d5d2d22516c5 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 23 Oct 2025 08:44:55 -0300 Subject: [PATCH 63/90] chore(workflow): enhance GitHub Actions workflow for Deno testing and coverage reporting - Updated the branches for push and pull_request events to include both "main" and "dev". - Improved clarity in step names, changing "Check out code" to "Setup repo" and "Set up Deno" to "Setup Deno". - Added steps for generating a coverage report and uploading it to Codecov. - Organized the workflow steps for better readability and maintainability. --- .github/workflows/test.yml | 48 +++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8d8c53f..88ab671 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,21 +1,53 @@ name: Test +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# This workflow will install Deno then run `deno lint` and `deno test`. +# For more information see: https://github.com/denoland/setup-deno + on: + push: + branches: ["main", "dev"] pull_request: - branches: ["main"] + branches: ["main", "dev"] + +permissions: + contents: read jobs: test: runs-on: ubuntu-latest + steps: - - name: Check out code + - name: Setup repo uses: actions/checkout@v4 - - name: Set up Deno + + - name: Setup Deno uses: denoland/setup-deno@v1 with: deno-version: v2.x - - name: Run unit tests with coverage - run: | - deno test --allow-all src/ --coverage=cov_profile - deno coverage cov_profile > coverage_summary.txt - awk '/total:/ { if ($2 < 75) exit 1 }' coverage_summary.txt + + # Uncomment this step to verify the use of 'deno fmt' on each commit. + # - name: Verify formatting + # run: deno fmt --check + + - name: Run linter + run: deno lint + + - name: Run tests + run: deno task test + + - name: Generate coverage report + run: deno coverage --lcov coverage > coverage.lcov + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + files: ./coverage.lcov + flags: unittests + name: codecov-umbrella + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} From c0fb0ffb3ff2f7cb2393cfec53e9a3580193dd75 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 23 Oct 2025 08:48:20 -0300 Subject: [PATCH 64/90] fix(deps): update @noble/curves and @noble/hashes dependencies to specific versions for improved stability and compatibility --- deno.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/deno.json b/deno.json index a9e9604..51cbdf0 100644 --- a/deno.json +++ b/deno.json @@ -16,14 +16,14 @@ "@fifo/convee": "jsr:@fifo/convee@^0.5.0", "@colibri/core": "jsr:@colibri/core@^0.5.3", - "@noble/curves": "jsr:@noble/curves", - "@noble/hashes": "jsr:@noble/hashes", + "@noble/curves": "jsr:@noble/curves@^1.8.0", + "@noble/curves/p256": "jsr:@noble/curves@^1.8.0/p256", + "@noble/curves/abstract/modular": "jsr:@noble/curves@^1.8.0/abstract/modular", + "@noble/hashes": "jsr:@noble/hashes@^1.6.1", + "@noble/hashes/sha256": "jsr:@noble/hashes@^1.6.1/sha256", + "@noble/hashes/hkdf": "jsr:@noble/hashes@^1.6.1/hkdf", "@noble/secp256k1": "jsr:@noble/secp256k1", "@stellar/stellar-sdk": "npm:@stellar/stellar-sdk@^14.2.0", - "jsr:@noble/curves/p256": "jsr:@noble/curves/p256@latest", - "jsr:@noble/hashes/sha256": "jsr:@noble/hashes/sha256@latest", - "jsr:@noble/hashes/hkdf": "jsr:@noble/hashes/hkdf@latest", - "jsr:@noble/curves/abstract/modular": "jsr:@noble/curves/abstract/modular@latest", "@std/assert": "jsr:@std/assert@^1.0.0", "@std/testing": "jsr:@std/testing@^1.0.0", From 3e100deea8ea9665a4e248af1b8b50f72c20db75 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 23 Oct 2025 11:16:25 -0300 Subject: [PATCH 65/90] feat: restructure exports and enhance integration tests for channel and privacy modules - Updated `mod.ts` to include exports from channel-auth, conditions, privacy-channel, operation, and transaction-builder modules for better modularity. - Added `src/utils/secp256r1/index.ts` to centralize secp256r1 utility functions. - Modified integration tests in `channel-auth.integration.test.ts` and `privacy-channel.integration.test.ts` to use new types from the updated module structure. - Refactored constructor argument types in integration tests for clarity and consistency. - Improved import statements across various test files to streamline dependencies and enhance readability. --- mod.ts | 19 +++++++-- src/utils/index.ts | 5 ++- src/utils/secp256r1/index.ts | 5 +++ .../channel-auth.integration.test.ts | 19 +++++---- .../privacy-channel.integration.test.ts | 25 ++++++------ .../utxo-based-account.integration.test.ts | 39 +++++++++---------- 6 files changed, 66 insertions(+), 46 deletions(-) create mode 100644 src/utils/secp256r1/index.ts diff --git a/mod.ts b/mod.ts index e79a7ff..b08fcf4 100644 --- a/mod.ts +++ b/mod.ts @@ -1,7 +1,20 @@ // export * from "./src/pool/permissionlessPoolClient.ts"; -export * from "./src/pool/index.ts"; -export * from "./src/pool/types.ts"; +export * from "./src/channel-auth/index.ts"; +export * as AuthTypes from "./src/channel-auth/types.ts"; +export * from "./src/channel-auth/constants.ts"; + +export * from "./src/conditions/index.ts"; +export * as ConditionTypes from "./src/conditions/types.ts"; + +export * from "./src/privacy-channel/index.ts"; +export * from "./src/privacy-channel/constants.ts"; +export * as ChannelTypes from "./src/privacy-channel/types.ts"; + +export * from "./src/operation/index.ts"; +export * as OperationTypes from "./src/operation/types.ts"; + +export * from "./src/transaction-builder/index.ts"; export * from "./src/core/utxo-keypair-base/types.ts"; export * from "./src/core/utxo-keypair-base/index.ts"; @@ -19,5 +32,3 @@ export * from "./src/utxo-based-account/utxo-based-stellar-account/index.ts"; // export * from "./executor/client.ts"; export * from "./src/utils/index.ts"; - -export * from "./src/utils/types/stellar.types.ts"; diff --git a/src/utils/index.ts b/src/utils/index.ts index 3a32afb..c685011 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,4 @@ export * from "./regex/stellar/gAccountPublicKey.ts"; - -export * from "./hash/sha256Hash.ts"; \ No newline at end of file +export * from "./secp256r1/index.ts"; +export * from "./hash/sha256Hash.ts"; +export * from "./common/index.ts"; diff --git a/src/utils/secp256r1/index.ts b/src/utils/secp256r1/index.ts new file mode 100644 index 0000000..799921a --- /dev/null +++ b/src/utils/secp256r1/index.ts @@ -0,0 +1,5 @@ +export * from "./deriveP256KeyPairFromSeed.ts"; +export * from "./encodeECPrivateKey.ts"; +export * from "./encodePKCS8.ts"; +export * from "./generateP256KeyPair.ts"; +export * from "./signPayload.ts"; diff --git a/test/integration/channel-auth.integration.test.ts b/test/integration/channel-auth.integration.test.ts index 71e63c7..83df8d2 100644 --- a/test/integration/channel-auth.integration.test.ts +++ b/test/integration/channel-auth.integration.test.ts @@ -14,15 +14,18 @@ import type { TransactionConfig, ContractId, } from "@colibri/core"; -import { - AuthInvokeMethods, - AuthReadMethods, - AuthSpec, -} from "../../src/channel-auth/constants.ts"; + import type { Buffer } from "node:buffer"; import { loadContractWasm } from "../helpers/load-wasm.ts"; -import { ChannelAuth } from "../../src/channel-auth/index.ts"; -import type { ChannelAuthConstructorArgs } from "../../src/channel-auth/types.ts"; + +import { + AuthSpec, + AuthReadMethods, + AuthInvokeMethods, + ChannelAuth, + type ChannelTypes, +} from "../../mod.ts"; + import { disableSanitizeConfig } from "../utils/disable-sanitize-config.ts"; describe("[Testnet - Integration] ChannelAuth", disableSanitizeConfig, () => { @@ -76,7 +79,7 @@ describe("[Testnet - Integration] ChannelAuth", disableSanitizeConfig, () => { config: txConfig, constructorArgs: { admin: admin.address() as Ed25519PublicKey, - } as ChannelAuthConstructorArgs, + } as ChannelTypes.ChannelConstructorArgs, }); authId = authContract.getContractId(); diff --git a/test/integration/privacy-channel.integration.test.ts b/test/integration/privacy-channel.integration.test.ts index 380e4c6..97a827d 100644 --- a/test/integration/privacy-channel.integration.test.ts +++ b/test/integration/privacy-channel.integration.test.ts @@ -23,21 +23,22 @@ import { } from "../../src/channel-auth/constants.ts"; import type { Buffer } from "node:buffer"; import { loadContractWasm } from "../helpers/load-wasm.ts"; -import type { ChannelAuthConstructorArgs } from "../../src/channel-auth/types.ts"; -import type { ChannelConstructorArgs } from "../../src/privacy-channel/types.ts"; import { - ChannelInvokeMethods, - ChannelReadMethods, ChannelSpec, -} from "../../src/privacy-channel/constants.ts"; + type ChannelTypes, + PrivacyChannel, + ChannelReadMethods, + generateP256KeyPair, + MoonlightTransactionBuilder, + MoonlightOperation as op, + generateNonce, + ChannelInvokeMethods, +} from "../../mod.ts"; + import { Asset, Keypair } from "@stellar/stellar-sdk"; -import { PrivacyChannel } from "../../src/privacy-channel/index.ts"; + import { disableSanitizeConfig } from "../utils/disable-sanitize-config.ts"; -import { generateP256KeyPair } from "../../src/utils/secp256r1/generateP256KeyPair.ts"; -import { MoonlightTransactionBuilder } from "../../src/transaction-builder/index.ts"; import { Server } from "@stellar/stellar-sdk/rpc"; -import { generateNonce } from "../../src/utils/common/index.ts"; -import { MoonlightOperation as op } from "../../src/operation/index.ts"; describe( "[Testnet - Integration] PrivacyChannel", @@ -113,7 +114,7 @@ describe( config: txConfig, constructorArgs: { admin: admin.address() as Ed25519PublicKey, - } as ChannelAuthConstructorArgs, + } as ChannelTypes.ChannelConstructorArgs, }); authId = authContract.getContractId(); @@ -147,7 +148,7 @@ describe( admin: admin.address() as Ed25519PublicKey, auth_contract: authId, asset: assetId, - } as ChannelConstructorArgs, + } as ChannelTypes.ChannelConstructorArgs, }); channelId = channelContract.getContractId(); diff --git a/test/integration/utxo-based-account.integration.test.ts b/test/integration/utxo-based-account.integration.test.ts index 2e19f52..90d04f1 100644 --- a/test/integration/utxo-based-account.integration.test.ts +++ b/test/integration/utxo-based-account.integration.test.ts @@ -15,30 +15,29 @@ import { type TestNetConfig, type Ed25519SecretKey, } from "@colibri/core"; -import { - AuthInvokeMethods, - AuthSpec, -} from "../../src/channel-auth/constants.ts"; + import { Buffer } from "node:buffer"; import { loadContractWasm } from "../helpers/load-wasm.ts"; -import type { ChannelAuthConstructorArgs } from "../../src/channel-auth/types.ts"; -import type { ChannelConstructorArgs } from "../../src/privacy-channel/types.ts"; +import { Asset, Keypair } from "@stellar/stellar-sdk"; +import { Server } from "@stellar/stellar-sdk/rpc"; +import { disableSanitizeConfig } from "../utils/disable-sanitize-config.ts"; + import { + AuthSpec, + AuthInvokeMethods, ChannelInvokeMethods, ChannelReadMethods, ChannelSpec, -} from "../../src/privacy-channel/constants.ts"; -import { Asset, Keypair } from "@stellar/stellar-sdk"; -import { PrivacyChannel } from "../../src/privacy-channel/index.ts"; -import { UtxoBasedStellarAccount } from "../../src/utxo-based-account/utxo-based-stellar-account/index.ts"; -import { UTXOStatus } from "../../src/core/utxo-keypair/types.ts"; -import { StellarDerivator } from "../../src/derivation/stellar/index.ts"; -import { MoonlightTransactionBuilder } from "../../src/transaction-builder/index.ts"; -import { MoonlightOperation as op } from "../../src/operation/index.ts"; -import { generateNonce } from "../../src/utils/common/index.ts"; -import { Server } from "@stellar/stellar-sdk/rpc"; -import { disableSanitizeConfig } from "../utils/disable-sanitize-config.ts"; -import { StellarNetworkId } from "../../src/derivation/stellar/stellar-network-id.ts"; + UTXOStatus, + MoonlightTransactionBuilder, + MoonlightOperation as op, + generateNonce, + StellarDerivator, + StellarNetworkId, + UtxoBasedStellarAccount, + type ChannelTypes, + PrivacyChannel, +} from "../../mod.ts"; describe( "[Testnet - Integration] UtxoBasedAccount", @@ -114,7 +113,7 @@ describe( config: txConfig, constructorArgs: { admin: admin.address() as Ed25519PublicKey, - } as ChannelAuthConstructorArgs, + } as ChannelTypes.ChannelConstructorArgs, }); authId = authContract.getContractId(); @@ -153,7 +152,7 @@ describe( admin: admin.address() as Ed25519PublicKey, auth_contract: authId, asset: assetId, - } as ChannelConstructorArgs, + } as ChannelTypes.ChannelConstructorArgs, }); channelId = channelContract.getContractId(); From aa9454ccd23c182584147042df3a2b4e4c294d65 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 23 Oct 2025 11:20:56 -0300 Subject: [PATCH 66/90] fix: enhance type definitions and return types in various modules - Updated `AuthSpec` and `ChannelSpec` in `constants.ts` to explicitly define type as `Spec`. - Modified return types in `read` methods of `ChannelAuth` and `PrivacyChannel` classes to use `ReturnType`. - Improved method signatures in `MoonlightTransactionBuilder` to return `MoonlightTransactionBuilder` for better method chaining. - Added import for `UTXOPublicKey` in `generateP256KeyPair.ts` to ensure proper type usage. These changes improve type safety and clarity across the codebase, facilitating better integration and usage of the SDK. --- src/channel-auth/constants.ts | 2 +- src/channel-auth/index.ts | 2 +- src/privacy-channel/constants.ts | 2 +- src/privacy-channel/index.ts | 4 ++-- src/transaction-builder/index.ts | 16 ++++++++-------- src/utils/secp256r1/generateP256KeyPair.ts | 11 ++++++++--- 6 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/channel-auth/constants.ts b/src/channel-auth/constants.ts index d502fe7..386f84d 100644 --- a/src/channel-auth/constants.ts +++ b/src/channel-auth/constants.ts @@ -12,7 +12,7 @@ export const enum AuthInvokeMethods { remove_provider = "remove_provider", } -export const AuthSpec = new Spec([ +export const AuthSpec: Spec = new Spec([ "AAAAAAAAAAAAAAAFYWRtaW4AAAAAAAAAAAAAAQAAABM=", "AAAAAAAAAAAAAAAJc2V0X2FkbWluAAAAAAAAAQAAAAAAAAAJbmV3X2FkbWluAAAAAAAAEwAAAAA=", "AAAAAAAAAAAAAAAHdXBncmFkZQAAAAABAAAAAAAAAAl3YXNtX2hhc2gAAAAAAAPuAAAAIAAAAAA=", diff --git a/src/channel-auth/index.ts b/src/channel-auth/index.ts index 8cd4f54..17478b8 100644 --- a/src/channel-auth/index.ts +++ b/src/channel-auth/index.ts @@ -119,7 +119,7 @@ export class ChannelAuth { method: M; methodArgs: AuthInvoke[M]["input"]; config: TransactionConfig; - }) { + }): Promise> { return await this.getClient().invoke(args); } } diff --git a/src/privacy-channel/constants.ts b/src/privacy-channel/constants.ts index f5779d8..7da9d9b 100644 --- a/src/privacy-channel/constants.ts +++ b/src/privacy-channel/constants.ts @@ -16,7 +16,7 @@ export const enum ChannelInvokeMethods { set_auth = "set_auth", } -export const ChannelSpec = new Spec([ +export const ChannelSpec: Spec = new Spec([ "AAAAAAAAAAAAAAAFYWRtaW4AAAAAAAAAAAAAAQAAABM=", "AAAAAAAAAAAAAAAJc2V0X2FkbWluAAAAAAAAAQAAAAAAAAAJbmV3X2FkbWluAAAAAAAAEwAAAAA=", "AAAAAAAAAAAAAAAHdXBncmFkZQAAAAABAAAAAAAAAAl3YXNtX2hhc2gAAAAAAAPuAAAAIAAAAAA=", diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index 2180102..5475131 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -178,7 +178,7 @@ export class PrivacyChannel { methodArgs: ChannelInvoke[M]["input"]; auth?: xdr.SorobanAuthorizationEntry[]; config: TransactionConfig; - }) { + }): Promise> { return await this.getClient().invoke(args); } @@ -200,7 +200,7 @@ export class PrivacyChannel { auth?: xdr.SorobanAuthorizationEntry[]; }; config: TransactionConfig; - }) { + }): Promise> { return await this.getClient().invokeRaw(args); } } diff --git a/src/transaction-builder/index.ts b/src/transaction-builder/index.ts index 6e2bbdc..30b6fbd 100644 --- a/src/transaction-builder/index.ts +++ b/src/transaction-builder/index.ts @@ -302,7 +302,7 @@ export class MoonlightTransactionBuilder { // - Methods //========================================== - addOperation(op: MoonlightOperation) { + addOperation(op: MoonlightOperation): MoonlightTransactionBuilder { if (op.isCreate()) return this.addCreate(op); if (op.isSpend()) return this.addSpend(op); if (op.isDeposit()) return this.addDeposit(op); @@ -310,7 +310,7 @@ export class MoonlightTransactionBuilder { throw new Error("Unsupported operation type"); } - private addCreate(op: CreateOperation) { + private addCreate(op: CreateOperation): MoonlightTransactionBuilder { assertNoDuplicateCreate(this.getCreateOperations(), op); assertPositiveAmount(op.getAmount(), "Create operation"); @@ -318,14 +318,14 @@ export class MoonlightTransactionBuilder { return this; } - private addSpend(op: SpendOperation) { + private addSpend(op: SpendOperation): MoonlightTransactionBuilder { assertNoDuplicateSpend(this.getSpendOperations(), op); this.setSpendOperations([...this.getSpendOperations(), op]); return this; } - private addDeposit(op: DepositOperation) { + private addDeposit(op: DepositOperation): MoonlightTransactionBuilder { assertNoDuplicatePubKey(this.getDepositOperations(), op, "Deposit"); assertPositiveAmount(op.getAmount(), "Deposit operation"); @@ -333,7 +333,7 @@ export class MoonlightTransactionBuilder { return this; } - private addWithdraw(op: WithdrawOperation) { + private addWithdraw(op: WithdrawOperation): MoonlightTransactionBuilder { assertNoDuplicatePubKey(this.getWithdrawOperations(), op, "Withdraw"); assertPositiveAmount(op.getAmount(), "Withdraw operation"); @@ -345,7 +345,7 @@ export class MoonlightTransactionBuilder { utxo: UTXOPublicKey, signature: Buffer, expirationLedger: number - ) { + ): MoonlightTransactionBuilder { assertSpendExists(this.getSpendOperations(), utxo); this.innerSignatures.set(utxo, { @@ -360,7 +360,7 @@ export class MoonlightTransactionBuilder { signature: Buffer, expirationLedger: number, nonce: string - ) { + ): MoonlightTransactionBuilder { this.providerInnerSignatures.set(pubKey, { sig: signature, exp: expirationLedger, @@ -372,7 +372,7 @@ export class MoonlightTransactionBuilder { public addExtSignedEntry( pubKey: Ed25519PublicKey, signedAuthEntry: xdr.SorobanAuthorizationEntry - ) { + ): MoonlightTransactionBuilder { if ( !this.getDepositOperations().find((d) => d.getPublicKey() === pubKey) && !this.getWithdrawOperations().find((d) => d.getPublicKey() === pubKey) diff --git a/src/utils/secp256r1/generateP256KeyPair.ts b/src/utils/secp256r1/generateP256KeyPair.ts index 7a86a93..c5090fd 100644 --- a/src/utils/secp256r1/generateP256KeyPair.ts +++ b/src/utils/secp256r1/generateP256KeyPair.ts @@ -1,17 +1,22 @@ -export async function generateP256KeyPair() { +import type { UTXOPublicKey } from "../../core/utxo-keypair-base/types.ts"; + +export async function generateP256KeyPair(): Promise<{ + privateKey: Uint8Array; + publicKey: UTXOPublicKey; +}> { const keyPair = await crypto.subtle.generateKey( { name: "ECDSA", namedCurve: "P-256", }, true, // Extractable - ["sign", "verify"], // Key usages + ["sign", "verify"] // Key usages ); // Export keys as raw and PKCS8 format const privateKey = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey); const publicKey = new Uint8Array( - await crypto.subtle.exportKey("raw", keyPair.publicKey), + await crypto.subtle.exportKey("raw", keyPair.publicKey) ); return { From 4cac6657a3a920b0d2d8f528a91978e2b912538a Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 23 Oct 2025 11:21:17 -0300 Subject: [PATCH 67/90] =?UTF-8?q?refactor:=20remove=20commented-out=20expo?= =?UTF-8?q?rt=20statements=20in=20mod.ts=20for=20cleaner=20codebase=20?= =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mod.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mod.ts b/mod.ts index b08fcf4..672f216 100644 --- a/mod.ts +++ b/mod.ts @@ -1,5 +1,3 @@ -// export * from "./src/pool/permissionlessPoolClient.ts"; - export * from "./src/channel-auth/index.ts"; export * as AuthTypes from "./src/channel-auth/types.ts"; export * from "./src/channel-auth/constants.ts"; @@ -29,6 +27,4 @@ export * from "./src/core/utxo-keypair-base/types.ts"; export * from "./src/utxo-based-account/index.ts"; export * from "./src/utxo-based-account/utxo-based-stellar-account/index.ts"; -// export * from "./executor/client.ts"; - export * from "./src/utils/index.ts"; From 8cea414778930ec563f3a0144f031e19af897356 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 23 Oct 2025 11:33:59 -0300 Subject: [PATCH 68/90] fix: update Deno version to 2.2.2 and change publish command to use Deno --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 46fe994..9fecc30 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,7 +20,7 @@ jobs: - name: Setup Deno uses: denoland/setup-deno@v2 with: - deno-version: v1.x + deno-version: 2.2.2 - name: Detect version bump run: | @@ -32,7 +32,7 @@ jobs: fi - name: Publish package - run: npx jsr publish --allow-slow-types + run: deno publish --allow-slow-types - name: Create tag and release if: env.VERSION_IS_UPDATED == 'true' From 85cfed501c4af85556efa342734ef0c54046e99f Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:26:23 -0300 Subject: [PATCH 69/90] =?UTF-8?q?feat:=20implement=20custom=20error=20hand?= =?UTF-8?q?ling=20with=20MoonlightError=20class=20and=20r=E2=80=A6=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: implement custom error handling with MoonlightError class and related types - Added `MoonlightError` class to provide structured error handling. - Introduced `GeneralErrorCode` enum for standardized error codes. - Created `GEN_ERRORS` constant for mapping error codes to `MoonlightError`. - Defined `MoonlightErrorShape` interface for error structure. - Implemented `Diagnostic` type for detailed error context. - Added tests for `MoonlightError` constructor, serialization, and utility methods. - Updated `mod.ts` to export new error handling modules. * fix: update error source to use "@Moonlight" for consistency in MoonlightError class 🛠️ * feat: add assertion utilities for argument validation and condition checks - Implemented `assertRequiredArgs` function in `assert-args.ts` to ensure all provided arguments are neither null nor undefined, throwing a specified error if any argument is invalid. - Added `assert` function in `assert.ts` to validate that a given condition is true, throwing the provided error if the condition is false. - Updated `index.ts` to export both new assertion functions for easier access. * feat: enhance UTXOKeypair tests and add error handling - Refactored tests in `src/core/utxo-keypair/index.unit.test.ts` to use BDD style with `describe` and `it` for better readability and structure. - Added assertions for UTXOKeypair's constructor, state updates, and status helper methods. - Introduced new error handling in `src/derivation/error.ts` with custom error classes for property validation. - Updated `src/derivation/base/index.ts` to utilize new error handling methods and improve property checks. - Enhanced error tests in `src/derivation/base/index.unit.test.ts` to validate new error types. - Updated error aggregation in `src/error/errors.ts` to include derivation errors. * refactor: remove error handling from auto-load in UTXOKeypair constructor for cleaner execution * feat: implement UTXOKeypair error handling and validation - Added UTXOKeypairError class and related error types in src/core/utxo-keypair/error.ts to manage errors specific to UTXOKeypair operations. - Introduced DERIVATOR_NOT_CONFIGURED error to handle cases where the derivator is not properly configured. - Updated UTXOKeypair methods to use assertion utilities for better error handling. - Modified error handling in tests to utilize the new error structure. - Enhanced error domain to include "utxo-keypair" in src/error/types.ts for comprehensive error categorization. * refactor(assert): update error type to Error for improved flexibility and clarity * feat(operation): add error handling for operation-related issues - Introduced OPR_ERRORS for managing operation-specific errors in src/operation/error.ts. - Updated ErrorDomain in src/error/types.ts to include "operation". - Enhanced MoonlightOperation class in src/operation/index.ts with assertions for amount and public key validity, throwing specific operation errors. - Modified unit tests in src/transaction-builder/index.unit.test.ts to validate error handling for zero and negative amounts in CREATE, DEPOSIT, and WITHDRAW operations. * feat(privacy-channel): add PrivacyChannelError handling and integrate with PrivacyChannel - Introduced PrivacyChannelError and associated types in src/privacy-channel/error.ts for better error management. - Updated ErrorDomain in src/error/types.ts to include "privacy-channel". - Enhanced error handling in PrivacyChannel class by throwing PROPERTY_NOT_SET error when accessing unset properties. - Consolidated error exports in src/error/errors.ts to include new privacy channel errors. * feat(transaction-builder): implement error handling for transaction operations - Added new error types for transaction builder operations in `src/transaction-builder/error.ts`, including: - `PROPERTY_NOT_SET` - `UNSUPPORTED_OP_TYPE` - `DUPLICATE_CREATE_OP` - `DUPLICATE_SPEND_OP` - `DUPLICATE_DEPOSIT_OP` - `DUPLICATE_WITHDRAW_OP` - `NO_SPEND_OPS` - `NO_DEPOSIT_OPS` - `NO_WITHDRAW_OPS` - `NO_EXT_OPS` - `MISSING_PROVIDER_SIGNATURE` - Updated `src/transaction-builder/index.ts` to utilize the new error types for better error handling in methods like `addOperation`, `addDeposit`, and `addWithdraw`. - Refactored validation functions in `src/transaction-builder/validators/operations.ts` to throw specific errors instead of generic ones, enhancing clarity and maintainability. - Enhanced unit tests in `src/transaction-builder/index.unit.test.ts` to assert the new error types for various operations, ensuring robust error handling. * feat(utxo-based-account): add error handling for UTXO-based account operations - Implemented custom error classes for UTXO-based account errors, including: - NEGATIVE_INDEX for negative index values - UTXO_TO_DERIVE_TOO_LOW for insufficient UTXOs to derive - MISSING_BATCH_FETCH_FN for missing batch fetch function - MISSING_UTXO_FOR_INDEX for missing UTXO at a given index - Updated error handling in UtxoBasedAccount class methods to utilize new error classes. - Enhanced error domain in types to include "utxo-based-account". - Integrated new error handling in unit tests for improved reliability and clarity. * chore(deno.json): update version to 0.4.0 for release 🚀 * refactor(lint): fix linter issues * refactor(settings): remove unnecessary instruction about emoji usage * feat(operation): enhance signature management - Added new error codes and corresponding error classes in `src/operation/error.ts`: - OP_IS_NOT_SIGNABLE - OP_HAS_NO_CONDITIONS - SIGNER_IS_NOT_DEPOSITOR - OP_ALREADY_SIGNED - Updated `src/operation/index.ts` to include methods for managing UTXO and Ed25519 signatures in `MoonlightOperation` class: - `signWithUTXO` - `signWithEd25519` - `getUTXOSignature` - `getEd25519Signature` - `isSignedByUTXO` - `isSignedByEd25519` - Modified `src/operation/types.ts` to reflect new signature management methods and types. - Refactored `src/transaction-builder/index.ts` to utilize new signature management methods and ensure proper handling of signatures during transaction building. - Updated unit tests in `src/transaction-builder/index.unit.test.ts` to cover new signature functionalities and error handling scenarios. - Adjusted integration tests in `test/integration/privacy-channel.integration.test.ts` and `test/integration/utxo-based-account.integration.test.ts` to use asset IDs instead of asset objects for consistency. * feat(condition): implement MLXDR conversion methods and enhance condition handling - Added MLXDR conversion methods in the Condition class for custom Moonlight XDR format. - Introduced fromMLXDR and toMLXDR methods to facilitate MLXDR serialization and deserialization. - Enhanced fromScVal method to handle various UTXO operation types (CREATE, DEPOSIT, WITHDRAW). - Updated BaseCondition interface to include toMLXDR method. - Added tests to ensure data integrity during XDR and MLXDR conversions in index.unit.test.ts. - Created custom-xdr module for MLXDR handling with type definitions and utility functions. - Defined MLXDRTypeByte enum and MLXDRPrefix for identifying custom XDR types. * feat: Enhance MLXDR support for operations and conditions - Refactor MLXDR type bytes to include specific operation types (Create, Deposit, Withdraw, Spend). - Implement conversion functions for operations to and from MLXDR format. - Add error handling for invalid SCVal types and vectors in operations. - Update MoonlightOperation class to support MLXDR conversions. - Create unit tests for MLXDR conversions for all operation types, ensuring correct serialization and deserialization. - Improve type safety by using type imports for various entities. * feat(coverage): add initial code coverage configuration with target set to 70% --- .vscode/settings.json | 3 - codecov.yml | 5 + deno.json | 2 +- mod.ts | 7 + src/conditions/index.ts | 97 ++++- src/conditions/index.unit.test.ts | 134 ++++++ src/conditions/types.ts | 8 +- src/core/utxo-keypair/error.ts | 82 ++++ src/core/utxo-keypair/index.ts | 14 +- src/core/utxo-keypair/index.unit.test.ts | 264 +++++++----- src/custom-xdr/index.ts | 342 +++++++++++++++ src/custom-xdr/types.ts | 21 + src/derivation/base/index.ts | 117 ++++-- src/derivation/base/index.unit.test.ts | 13 +- src/derivation/error.ts | 86 ++++ src/error/errors.ts | 16 + src/error/index.ts | 143 +++++++ src/error/index.unit.test.ts | 184 ++++++++ src/error/types.ts | 32 ++ src/operation/error.ts | 281 +++++++++++++ src/operation/index.ts | 397 +++++++++++++++++- src/operation/index.unit.test.ts | 375 +++++++++++++++++ src/operation/types.ts | 36 +- src/privacy-channel/error.ts | 56 +++ src/privacy-channel/index.ts | 3 +- src/transaction-builder/auth/index.ts | 2 +- src/transaction-builder/error.ts | 217 ++++++++++ src/transaction-builder/index.ts | 195 +++++---- src/transaction-builder/index.unit.test.ts | 212 ++++------ .../validators/operations.ts | 54 ++- src/utils/assert/assert-args.ts | 13 + src/utils/assert/assert.ts | 13 + src/utils/assert/index.ts | 2 + .../auth/deposit-auth-entry copy.ts} | 0 src/utxo-based-account/error.ts | 96 +++++ src/utxo-based-account/index.ts | 21 +- src/utxo-based-account/index.unit.test.ts | 7 +- .../privacy-channel.integration.test.ts | 4 +- .../utxo-based-account.integration.test.ts | 32 +- 39 files changed, 3116 insertions(+), 470 deletions(-) create mode 100644 codecov.yml create mode 100644 src/core/utxo-keypair/error.ts create mode 100644 src/custom-xdr/index.ts create mode 100644 src/custom-xdr/types.ts create mode 100644 src/derivation/error.ts create mode 100644 src/error/errors.ts create mode 100644 src/error/index.ts create mode 100644 src/error/index.unit.test.ts create mode 100644 src/error/types.ts create mode 100644 src/operation/error.ts create mode 100644 src/operation/index.unit.test.ts create mode 100644 src/privacy-channel/error.ts create mode 100644 src/transaction-builder/error.ts create mode 100644 src/utils/assert/assert-args.ts create mode 100644 src/utils/assert/assert.ts create mode 100644 src/utils/assert/index.ts rename src/{transaction-builder/auth/deposit-auth-entry.ts => utils/auth/deposit-auth-entry copy.ts} (100%) create mode 100644 src/utxo-based-account/error.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index f11c7e9..20a5837 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,9 +10,6 @@ }, { "text": "Be extremely detailed with the file changes and the reason for the change." - }, - { - "text": "Add emoji to simbolize the type of change or context. Never humorous." } ] } diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..25f3570 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,5 @@ +coverage: + status: + patch: + default: + target: 70% diff --git a/deno.json b/deno.json index 51cbdf0..9965b5c 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@moonlight/moonlight-sdk", - "version": "0.3.0", + "version": "0.4.0", "description": "A privacy-focused toolkit for the Moonlight protocol on Stellar Soroban smart contracts.", "license": "MIT", "tasks": { diff --git a/mod.ts b/mod.ts index 672f216..31a509e 100644 --- a/mod.ts +++ b/mod.ts @@ -28,3 +28,10 @@ export * from "./src/utxo-based-account/index.ts"; export * from "./src/utxo-based-account/utxo-based-stellar-account/index.ts"; export * from "./src/utils/index.ts"; + +export * from "./src/error/index.ts"; +export type * from "./src/error/types.ts"; +export * from "./src/error/errors.ts"; + +export * from "./src/custom-xdr/index.ts"; +export * from "./src/custom-xdr/types.ts"; diff --git a/src/conditions/index.ts b/src/conditions/index.ts index 2e160e7..156dfdc 100644 --- a/src/conditions/index.ts +++ b/src/conditions/index.ts @@ -1,5 +1,10 @@ import { StrKey, type Ed25519PublicKey } from "@colibri/core"; -import { nativeToScVal, xdr } from "@stellar/stellar-sdk"; +import { + nativeToScVal, + scValToBigInt, + scValToNative, + xdr, +} from "@stellar/stellar-sdk"; import { Buffer } from "node:buffer"; import type { @@ -10,6 +15,7 @@ import type { } from "./types.ts"; import { UTXOOperationType } from "../operation/types.ts"; import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; +import { MLXDR } from "../custom-xdr/index.ts"; /** * Represents a condition for UTXO operations in the Moonlight privacy protocol. @@ -160,6 +166,78 @@ export class Condition implements BaseCondition { }) as WithdrawCondition; } + /** + * Creates a Condition instance from a base64-encoded XDR string. + * + * @param xdrString - The base64-encoded XDR representation of the condition + * @returns A Condition instance (CreateCondition, DepositCondition, or WithdrawCondition) + */ + public static fromXDR( + xdrString: string + ): CreateCondition | DepositCondition | WithdrawCondition { + const scVal = xdr.ScVal.fromXDR(xdrString, "base64"); + return Condition.fromScVal(scVal); + } + + /** + * Creates a Condition instance from a Moonlight XDR string. + * + * @param mlxdrString - The Moonlight XDR representation of the condition + * @returns A Condition instance (CreateCondition, DepositCondition, or WithdrawCondition) + */ + public static fromMLXDR( + mlxdrString: string + ): CreateCondition | DepositCondition | WithdrawCondition { + return MLXDR.toCondition(mlxdrString); + } + + /** + * + * Creates a Condition instance from a Stellar ScVal representation. + * @param scVal - The Stellar ScVal representation of the condition. + * @returns A Condition instance (CreateCondition, DepositCondition, or WithdrawCondition) + */ + public static fromScVal( + scVal: xdr.ScVal + ): CreateCondition | DepositCondition | WithdrawCondition { + if (scVal.switch().name !== xdr.ScValType.scvVec().name) { + throw new Error("Invalid ScVal type for Condition"); + } + + const vec = scVal.vec(); + + if (vec === null || vec.length !== 3) { + throw new Error("Invalid ScVal vector length for Condition"); + } + + const opScVal = vec[0]; + const addressScVal = vec[1]; + const amountScVal = vec[2]; + const amount = scValToBigInt(amountScVal); + + const opType = opScVal.sym().toString() as UTXOOperationType; + + if (opType === UTXOOperationType.CREATE) { + const utxo: UTXOPublicKey = Uint8Array.from(addressScVal.bytes()); + return Condition.create(utxo, amount); + } + + if (opType === UTXOOperationType.DEPOSIT) { + const address = scValToNative(addressScVal) as Ed25519PublicKey; + return Condition.deposit(address, amount); + } + + if (opType === UTXOOperationType.WITHDRAW) { + const address = scValToNative(addressScVal) as Ed25519PublicKey; + + return Condition.withdraw(address, amount); + } + + throw new Error( + `Unsupported operation type for Condition.fromScVal: ${opType}` + ); + } + //========================================== // Meta Requirement Methods //========================================== @@ -293,6 +371,12 @@ export class Condition implements BaseCondition { return conditionScVal; } + /** + * Converts this condition to a custom Moonlight XDR format. + * + */ + // public toMLXDR(): string {} + /** * Converts this condition to XDR (External Data Representation) format. * XDR is the serialization format used by the Stellar network for all data structures. @@ -311,6 +395,17 @@ export class Condition implements BaseCondition { return this.toScVal().toXDR("base64"); } + /** + * + * Converts this condition to Moonlight's custom MLXDR format. + * @returns The condition as a Moonlight XDR string + */ + public toMLXDR(): string { + return MLXDR.fromCondition( + this as CreateCondition | DepositCondition | WithdrawCondition + ); + } + //========================================== // Type Guard Methods //========================================== diff --git a/src/conditions/index.unit.test.ts b/src/conditions/index.unit.test.ts index a073f20..77afb94 100644 --- a/src/conditions/index.unit.test.ts +++ b/src/conditions/index.unit.test.ts @@ -6,6 +6,11 @@ import { Condition } from "./index.ts"; import { generateP256KeyPair } from "../utils/secp256r1/generateP256KeyPair.ts"; import { UTXOOperationType } from "../operation/types.ts"; import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; +import type { + CreateCondition, + DepositCondition, + WithdrawCondition, +} from "./types.ts"; describe("Condition", () => { let validPublicKey: Ed25519PublicKey; @@ -164,6 +169,135 @@ describe("Condition", () => { assertExists(scVal); assertEquals(scVal.switch().name, "scvVec"); }); + + it("should maintain data integrity through XDR conversion", () => { + const withdrawCondition = Condition.withdraw(validPublicKey, validAmount); + const withdrawScVal = withdrawCondition.toScVal(); + + const recreatedWithdrawCondition = Condition.fromScVal( + withdrawScVal + ) as WithdrawCondition; + + assertEquals( + recreatedWithdrawCondition.getOperation(), + withdrawCondition.getOperation() + ); + assertEquals( + recreatedWithdrawCondition.getAmount(), + withdrawCondition.getAmount() + ); + assertEquals( + recreatedWithdrawCondition.getPublicKey(), + withdrawCondition.getPublicKey() + ); + + const depositCondition = Condition.deposit(validPublicKey, validAmount); + const depositScVal = depositCondition.toScVal(); + + const recreatedDepositCondition = Condition.fromScVal( + depositScVal + ) as DepositCondition; + + assertEquals( + recreatedDepositCondition.getOperation(), + depositCondition.getOperation() + ); + assertEquals( + recreatedDepositCondition.getAmount(), + depositCondition.getAmount() + ); + assertEquals( + recreatedDepositCondition.getPublicKey(), + depositCondition.getPublicKey() + ); + + const createCondition = Condition.create(validUtxo, validAmount); + const createScVal = createCondition.toScVal(); + + const recreatedCreateCondition = Condition.fromScVal( + createScVal + ) as CreateCondition; + + assertEquals( + recreatedCreateCondition.getOperation(), + createCondition.getOperation() + ); + assertEquals( + recreatedCreateCondition.getAmount(), + createCondition.getAmount() + ); + assertEquals( + recreatedCreateCondition.getUtxo(), + createCondition.getUtxo() + ); + }); + + it("should maintain data integrity through MLXDR conversion", () => { + const createCondition = Condition.create(validUtxo, validAmount); + const mlxdrString = createCondition.toMLXDR(); + + const recreatedCreateCondition = Condition.fromMLXDR( + mlxdrString + ) as CreateCondition; + + assertEquals( + recreatedCreateCondition.getOperation(), + createCondition.getOperation() + ); + assertEquals( + recreatedCreateCondition.getAmount(), + createCondition.getAmount() + ); + assertEquals( + recreatedCreateCondition.getUtxo(), + createCondition.getUtxo() + ); + + const depositCondition = Condition.deposit(validPublicKey, validAmount); + const mlxdrStringDeposit = depositCondition.toMLXDR(); + + const recreatedDepositCondition = Condition.fromMLXDR( + mlxdrStringDeposit + ) as DepositCondition; + + assertEquals( + recreatedDepositCondition.getOperation(), + depositCondition.getOperation() + ); + assertEquals( + recreatedDepositCondition.getAmount(), + depositCondition.getAmount() + ); + assertEquals( + recreatedDepositCondition.getPublicKey(), + depositCondition.getPublicKey() + ); + + const withdrawCondition = Condition.withdraw(validPublicKey, validAmount); + const mlxdrStringWithdraw = withdrawCondition.toMLXDR(); + + const recreatedWithdrawCondition = Condition.fromMLXDR( + mlxdrStringWithdraw + ) as WithdrawCondition; + + assertEquals( + recreatedWithdrawCondition.getOperation(), + withdrawCondition.getOperation() + ); + assertEquals( + recreatedWithdrawCondition.getAmount(), + withdrawCondition.getAmount() + ); + assertEquals( + recreatedWithdrawCondition.getPublicKey(), + withdrawCondition.getPublicKey() + ); + + console.log("MLXDR Conversion tests passed"); + console.log(" CREATE MLXDR:", mlxdrString); + console.log("DEPOSIT MLXDR:", mlxdrStringDeposit); + console.log("WITHDRAW MLXDR:", mlxdrStringWithdraw); + }); }); describe("Errors", () => { diff --git a/src/conditions/types.ts b/src/conditions/types.ts index 68c07af..7b4162b 100644 --- a/src/conditions/types.ts +++ b/src/conditions/types.ts @@ -4,15 +4,19 @@ import type { xdr } from "@stellar/stellar-sdk"; import type { UTXOOperationType } from "../operation/types.ts"; import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; -export type BaseCondition = { +export interface BaseCondition { getOperation(): UTXOOperationType; getAmount(): bigint; isCreate(): this is CreateCondition; isDeposit(): this is DepositCondition; isWithdraw(): this is WithdrawCondition; toXDR(): string; + // fromXDR(xdrString: string): this; toScVal(): xdr.ScVal; -}; + // fromScVal(scVal: xdr.ScVal): this; + // fromMLXDR(mlxdrString: string): this; + toMLXDR(): string; +} export type CreateCondition = BaseCondition & { getOperation(): UTXOOperationType.CREATE; diff --git a/src/core/utxo-keypair/error.ts b/src/core/utxo-keypair/error.ts new file mode 100644 index 0000000..b53ca83 --- /dev/null +++ b/src/core/utxo-keypair/error.ts @@ -0,0 +1,82 @@ +import type { BaseDerivator } from "../../derivation/index.ts"; +import { MoonlightError } from "../../error/index.ts"; + +export type Meta = { + cause: Error | null; + data: unknown; +}; + +export type UTXOKeypairErrorShape = { + code: Code; + message: string; + details: string; + cause?: Error; + data: unknown; +}; + +export abstract class UTXOKeypairError< + Code extends string +> extends MoonlightError { + override readonly meta: Meta; + + constructor(args: UTXOKeypairErrorShape) { + const meta = { + cause: args.cause || null, + data: args.data, + }; + + super({ + domain: "utxo-keypair" as const, + source: "@Moonlight/utxo-keypair", + code: args.code, + message: args.message, + details: args.details, + meta, + }); + + this.meta = meta; + } +} + +export enum Code { + UNEXPECTED_ERROR = "UKP_000", + DERIVATOR_NOT_CONFIGURED = "UKP_001", +} + +// Currently unused, reserving +// +// export class UNEXPECTED_ERROR extends ContractError { +// constructor(cause: Error) { +// super({ +// code: Code.UNEXPECTED_ERROR, +// message: "An unexpected error occurred in the UTXOKeypair module!", +// details: "See the 'cause' for more details", +// cause, +// data: {}, +// }); +// } +// } + +export class DERIVATOR_NOT_CONFIGURED extends UTXOKeypairError { + constructor(derivator: BaseDerivator) { + super({ + code: Code.DERIVATOR_NOT_CONFIGURED, + message: `Derivator is not configured!`, + details: `The derivator provided to the UTXOKeypair is not properly configured. Check the derivator's context and root settings.`, + data: { + derivator: { + isRootSet: derivator.isSet("root"), + isContextSet: derivator.isSet("context"), + context: derivator.isSet("context") + ? derivator.getContext() + : undefined, + }, + }, + }); + } +} + +export const UKP_ERRORS = { + // [Code.UNEXPECTED_ERROR]: UNEXPECTED_ERROR, + [Code.DERIVATOR_NOT_CONFIGURED]: DERIVATOR_NOT_CONFIGURED, +}; diff --git a/src/core/utxo-keypair/index.ts b/src/core/utxo-keypair/index.ts index b55f7bf..78539b8 100644 --- a/src/core/utxo-keypair/index.ts +++ b/src/core/utxo-keypair/index.ts @@ -7,6 +7,8 @@ import { type UTXOKeypairOptions, UTXOStatus, } from "./types.ts"; +import * as E from "./error.ts"; +import { assert } from "../../utils/assert/assert.ts"; /** * Enhanced UTXOKeypair with state management capabilities @@ -50,9 +52,7 @@ export class UTXOKeypair< index: DIndex, options: UTXOKeypairOptions = {} ): Promise> { - if (!derivator.isConfigured()) { - throw new Error("Derivator is not properly configured"); - } + assert(derivator.isConfigured(), new E.DERIVATOR_NOT_CONFIGURED(derivator)); const context = derivator.getContext(); const keypair = await derivator.deriveKeypair(index); @@ -84,9 +84,7 @@ export class UTXOKeypair< count: number, options: UTXOKeypairOptions = {} ): Promise[]> { - if (!derivator.isConfigured()) { - throw new Error("Derivator is not properly configured"); - } + assert(derivator.isConfigured(), new E.DERIVATOR_NOT_CONFIGURED(derivator)); const utxos: UTXOKeypair[] = []; @@ -127,9 +125,7 @@ export class UTXOKeypair< // Auto-load if requested and balance fetcher is available if (options.autoLoad && this.balanceFetcher) { - this.load().catch((e) => { - console.error("Failed to auto-load UTXO state:", e); - }); + this.load(); } } diff --git a/src/core/utxo-keypair/index.unit.test.ts b/src/core/utxo-keypair/index.unit.test.ts index 5ca1aa4..0b03618 100644 --- a/src/core/utxo-keypair/index.unit.test.ts +++ b/src/core/utxo-keypair/index.unit.test.ts @@ -1,8 +1,9 @@ -// deno-lint-ignore-file require-await import { assertEquals, assertNotEquals, assertRejects } from "@std/assert"; +import { describe, it } from "@std/testing/bdd"; import { BaseDerivator } from "../../derivation/base/index.ts"; import { UTXOKeypair } from "./index.ts"; import { type BalanceFetcher, UTXOStatus } from "./types.ts"; +import * as UKP_ERR from "./error.ts"; // Mock key data for testing const mockPrivateKey = new Uint8Array([1, 2, 3, 4, 5]); @@ -29,7 +30,7 @@ class MockBalanceFetcher implements BalanceFetcher { this.fetchCount++; const key = Array.from(publicKey).toString(); if (this.balances.has(key)) { - return this.balances.get(key)!; + return await this.balances.get(key)!; } return 0n; } @@ -47,134 +48,178 @@ class TestDerivator extends BaseDerivator { override async deriveKeypair( _index: string ): Promise<{ publicKey: Uint8Array; privateKey: Uint8Array }> { - return { + return await { publicKey: mockPublicKey, privateKey: mockPrivateKey, }; } } -Deno.test("UTXOKeypair", async (t) => { - await t.step("constructor should initialize correctly", () => { - const utxo = new UTXOKeypair({ - privateKey: mockPrivateKey, - publicKey: mockPublicKey, - context: "test-context", - index: "0", +describe("UTXOKeypair", () => { + describe("constructor", () => { + it("initializes with correct default values", () => { + const utxo = new UTXOKeypair({ + privateKey: mockPrivateKey, + publicKey: mockPublicKey, + context: "test-context", + index: "0", + }); + + assertEquals(utxo.privateKey, mockPrivateKey); + assertEquals(utxo.publicKey, mockPublicKey); + assertEquals(utxo.context, "test-context"); + assertEquals(utxo.index, "0"); + assertEquals(utxo.status, UTXOStatus.UNLOADED); + assertEquals(utxo.balance, 0n); + assertEquals(utxo.decimals, 7); // Default value }); - assertEquals(utxo.privateKey, mockPrivateKey); - assertEquals(utxo.publicKey, mockPublicKey); - assertEquals(utxo.context, "test-context"); - assertEquals(utxo.index, "0"); - assertEquals(utxo.status, UTXOStatus.UNLOADED); - assertEquals(utxo.balance, 0n); - assertEquals(utxo.decimals, 7); // Default value + it("allows setting custom decimals", () => { + const utxo = new UTXOKeypair( + { + privateKey: mockPrivateKey, + publicKey: mockPublicKey, + context: "test-context", + index: "0", + }, + { decimals: 18 } + ); + + assertEquals(utxo.decimals, 18); + }); }); - await t.step("constructor should allow setting custom decimals", () => { - const utxo = new UTXOKeypair( - { + describe("updateState", () => { + it("updates balance and status correctly for positive balance", () => { + const utxo = new UTXOKeypair({ privateKey: mockPrivateKey, publicKey: mockPublicKey, context: "test-context", index: "0", - }, - { decimals: 18 } - ); + }); - assertEquals(utxo.decimals, 18); - }); + assertEquals(utxo.status, UTXOStatus.UNLOADED); - await t.step("updateState should update balance and status correctly", () => { - const utxo = new UTXOKeypair({ - privateKey: mockPrivateKey, - publicKey: mockPublicKey, - context: "test-context", - index: "0", + utxo.updateState(100n); + assertEquals(utxo.balance, 100n); + assertEquals(utxo.status, UTXOStatus.UNSPENT); + assertNotEquals(utxo.lastUpdated, 0); + }); + + it("updates status to FREE when balance is zero", () => { + const utxo = new UTXOKeypair({ + privateKey: mockPrivateKey, + publicKey: mockPublicKey, + context: "test-context", + index: "0", + }); + + utxo.updateState(0n); + assertEquals(utxo.balance, 0n); + assertEquals(utxo.status, UTXOStatus.FREE); }); - // Initially unloaded - assertEquals(utxo.status, UTXOStatus.UNLOADED); + it("updates status to SPENT when balance is negative", () => { + const utxo = new UTXOKeypair({ + privateKey: mockPrivateKey, + publicKey: mockPublicKey, + context: "test-context", + index: "0", + }); + + utxo.updateState(-1n); + assertEquals(utxo.balance, -1n); + assertEquals(utxo.status, UTXOStatus.SPENT); + }); + }); - // Update to an unspent status with positive balance - utxo.updateState(100n); - assertEquals(utxo.balance, 100n); - assertEquals(utxo.status, UTXOStatus.UNSPENT); + describe("load", () => { + it("updates state using balance fetcher", async () => { + const balanceFetcher = new MockBalanceFetcher(); + balanceFetcher.setBalance(mockPublicKey, 200n); - // Update to a free status with zero balance - utxo.updateState(0n); - assertEquals(utxo.balance, 0n); - assertEquals(utxo.status, UTXOStatus.FREE); + const utxo = new UTXOKeypair({ + privateKey: mockPrivateKey, + publicKey: mockPublicKey, + context: "test-context", + index: "0", + }); - // Update to a spent status with negative balance - utxo.updateState(-1n); - assertEquals(utxo.balance, -1n); - assertEquals(utxo.status, UTXOStatus.SPENT); + utxo.setBalanceFetcher(balanceFetcher); + await utxo.load(); - // lastUpdated should be set - assertNotEquals(utxo.lastUpdated, 0); + assertEquals(utxo.balance, 200n); + assertEquals(utxo.status, UTXOStatus.UNSPENT); + assertEquals(balanceFetcher.fetchCount, 1); + }); }); - await t.step("load should update state using fetcher", async () => { - const balanceFetcher = new MockBalanceFetcher(); - balanceFetcher.setBalance(mockPublicKey, 200n); + describe("status helper methods", () => { + it("correctly identifies UNLOADED status", () => { + const utxo = new UTXOKeypair({ + privateKey: mockPrivateKey, + publicKey: mockPublicKey, + context: "test-context", + index: "0", + }); - const utxo = new UTXOKeypair({ - privateKey: mockPrivateKey, - publicKey: mockPublicKey, - context: "test-context", - index: "0", + assertEquals(utxo.isUnloaded(), true); + assertEquals(utxo.isUnspent(), false); + assertEquals(utxo.isSpent(), false); + assertEquals(utxo.isFree(), false); }); - // Set the balance fetcher first, then load - utxo.setBalanceFetcher(balanceFetcher); - await utxo.load(); + it("correctly identifies UNSPENT status", () => { + const utxo = new UTXOKeypair({ + privateKey: mockPrivateKey, + publicKey: mockPublicKey, + context: "test-context", + index: "0", + }); + + utxo.updateState(100n); - assertEquals(utxo.balance, 200n); - assertEquals(utxo.status, UTXOStatus.UNSPENT); - assertEquals(balanceFetcher.fetchCount, 1); - }); + assertEquals(utxo.isUnloaded(), false); + assertEquals(utxo.isUnspent(), true); + assertEquals(utxo.isSpent(), false); + assertEquals(utxo.isFree(), false); + }); - await t.step("helper methods should correctly identify status", () => { - const utxo = new UTXOKeypair({ - privateKey: mockPrivateKey, - publicKey: mockPublicKey, - context: "test-context", - index: "0", + it("correctly identifies FREE status", () => { + const utxo = new UTXOKeypair({ + privateKey: mockPrivateKey, + publicKey: mockPublicKey, + context: "test-context", + index: "0", + }); + + utxo.updateState(0n); + + assertEquals(utxo.isUnloaded(), false); + assertEquals(utxo.isUnspent(), false); + assertEquals(utxo.isSpent(), false); + assertEquals(utxo.isFree(), true); }); - // Initially unloaded - assertEquals(utxo.isUnloaded(), true); - assertEquals(utxo.isUnspent(), false); - assertEquals(utxo.isSpent(), false); - assertEquals(utxo.isFree(), false); - - // Update to unspent with positive balance - utxo.updateState(100n); - assertEquals(utxo.isUnloaded(), false); - assertEquals(utxo.isUnspent(), true); - assertEquals(utxo.isSpent(), false); - assertEquals(utxo.isFree(), false); - - // Update to free with zero balance - utxo.updateState(0n); - assertEquals(utxo.isUnloaded(), false); - assertEquals(utxo.isUnspent(), false); - assertEquals(utxo.isSpent(), false); - assertEquals(utxo.isFree(), true); - - // Update to spent with negative balance - utxo.updateState(-1n); - assertEquals(utxo.isUnloaded(), false); - assertEquals(utxo.isUnspent(), false); - assertEquals(utxo.isSpent(), true); - assertEquals(utxo.isFree(), false); + it("correctly identifies SPENT status", () => { + const utxo = new UTXOKeypair({ + privateKey: mockPrivateKey, + publicKey: mockPublicKey, + context: "test-context", + index: "0", + }); + + utxo.updateState(-1n); + + assertEquals(utxo.isUnloaded(), false); + assertEquals(utxo.isUnspent(), false); + assertEquals(utxo.isSpent(), true); + assertEquals(utxo.isFree(), false); + }); }); - await t.step( - "fromDerivator should create keypair from derivator", - async () => { + describe("fromDerivator", () => { + it("creates keypair from configured derivator", async () => { const derivator = new TestDerivator("test-context", "secret-root"); const utxo = await UTXOKeypair.fromDerivator(derivator, "0"); @@ -188,25 +233,20 @@ Deno.test("UTXOKeypair", async (t) => { // The root should not be stored in the UTXOKeypair // deno-lint-ignore no-explicit-any assertEquals((utxo as any).root, undefined); - } - ); + }); - await t.step( - "fromDerivator should throw if derivator not configured", - async () => { + it("throws PROPERTY_NOT_SET when derivator not configured", async () => { const derivator = new BaseDerivator(); await assertRejects( - () => UTXOKeypair.fromDerivator(derivator, "0"), - Error, - "Derivator is not properly configured" + async () => await UTXOKeypair.fromDerivator(derivator, "0"), + UKP_ERR.DERIVATOR_NOT_CONFIGURED ); - } - ); + }); + }); - await t.step( - "deriveSequence should create multiple UTXOKeypairs", - async () => { + describe("deriveSequence", () => { + it("creates multiple UTXOKeypairs with sequential indices", async () => { const derivator = new TestDerivator("test-context", "secret-root"); const utxos = await UTXOKeypair.deriveSequence( @@ -228,6 +268,6 @@ Deno.test("UTXOKeypair", async (t) => { // deno-lint-ignore no-explicit-any assertEquals((utxo as any).root, undefined); } - } - ); + }); + }); }); diff --git a/src/custom-xdr/index.ts b/src/custom-xdr/index.ts new file mode 100644 index 0000000..f0287d9 --- /dev/null +++ b/src/custom-xdr/index.ts @@ -0,0 +1,342 @@ +/** + * + * Module for converting conditions to custom Moonlight XDR format. + * + * All custom XDR encoded for Moonlight are a BASE64 string prefixed by 'ML' to distinguish them from standard Stellar XDR. + * + * The first byte after the prefix indicates the object type: + * E.g: Given a Create Condition, the first byte after 'ML' would be 0x01. + * + * + * Example ML XDR encoding for a Spend Operation: + * - Decoded Bytes: + * - 0x30 0xb0: 'ML' Prefix + * - 0x05: Type Byte for Spend Operation + * - 0x...: Actual XDR Data + */ + +import { Buffer } from "buffer"; +import { MLXDRPrefix, MLXDRTypeByte } from "./types.ts"; +import type { Condition as ConditionType } from "../conditions/types.ts"; +import { nativeToScVal, scValToBigInt, xdr } from "@stellar/stellar-sdk"; +import { Condition } from "../conditions/index.ts"; +import { + type MoonlightOperation as MoonlightOperationType, + type OperationSignature, + UTXOOperationType, +} from "../operation/types.ts"; +import { MoonlightOperation } from "../operation/index.ts"; + +const MLXDRConditionBytes = [ + MLXDRTypeByte.CreateCondition, + MLXDRTypeByte.DepositCondition, + MLXDRTypeByte.WithdrawCondition, +]; + +const MLXDROperationBytes = [ + MLXDRTypeByte.CreateOperation, + MLXDRTypeByte.SpendOperation, + MLXDRTypeByte.DepositOperation, + MLXDRTypeByte.WithdrawOperation, +]; + +const MLXDRTransactionBundleBytes = [MLXDRTypeByte.TransactionBundle]; + +const isMLXDR = (data: string): boolean => { + const buffer = Buffer.from(data, "base64"); + + if (buffer.length < 2) { + return false; + } + const prefix = buffer.slice(0, 2); + return prefix.equals(MLXDRPrefix); +}; + +const getMLXDRTypePrefix = (data: string): Buffer => { + if (!isMLXDR(data)) { + throw new Error(`Data is not valid MLXDR format: ${data}`); + } + + const buffer = Buffer.from(data, "base64"); + + if (buffer.length < 3) { + throw new Error("Data is too short to contain MLXDR type prefix"); + } + + return buffer.slice(2, 3); +}; + +const appendMLXDRPrefixToRawXDR = ( + data: string, + typeByte: MLXDRTypeByte +): string => { + const rawBuffer = Buffer.from(data, "base64"); + + const prefix = Buffer.from([...MLXDRPrefix, typeByte]); + + const mlxdrBuffer = Buffer.alloc(rawBuffer.length + prefix.length); + + prefix.copy(mlxdrBuffer, 0); + rawBuffer.copy(mlxdrBuffer, prefix.length); + return mlxdrBuffer.toString("base64"); +}; + +const isCondition = (data: string): boolean => { + const typePrefix = getMLXDRTypePrefix(data); + + const prefixByte = typePrefix[0]; + return MLXDRConditionBytes.includes(prefixByte); +}; + +const isOperation = (data: string): boolean => { + const typePrefix = getMLXDRTypePrefix(data); + + const prefixByte = typePrefix[0]; + return MLXDROperationBytes.includes(prefixByte); +}; + +const isTransactionBundle = (data: string): boolean => { + const typePrefix = getMLXDRTypePrefix(data); + + const prefixByte = typePrefix[0]; + return MLXDRTransactionBundleBytes.includes(prefixByte); +}; + +const getXDRType = (data: string): MLXDRTypeByte | null => { + const typePrefix = getMLXDRTypePrefix(data); + + const prefixByte = typePrefix[0]; + if (typePrefix.length === 0) { + return null; + } + if (prefixByte >= 0x01 && prefixByte <= 0x0f) { + return prefixByte as MLXDRTypeByte; + } + return null; +}; + +const conditionToMLXDR = (condition: ConditionType): string => { + const rawScValXDR = condition.toXDR(); + + let typeByte: MLXDRTypeByte; + if (condition.isCreate()) typeByte = MLXDRTypeByte.CreateCondition; + else if (condition.isDeposit()) typeByte = MLXDRTypeByte.DepositCondition; + else if (condition.isWithdraw()) typeByte = MLXDRTypeByte.WithdrawCondition; + else throw new Error("Unknown condition type for MLXDR conversion"); + + return appendMLXDRPrefixToRawXDR(rawScValXDR, typeByte); +}; + +const MLXDRtoCondition = (data: string): ConditionType => { + if (!isCondition(data)) { + throw new Error("Data is not a valid MLXDR Condition"); + } + + const buffer = Buffer.from(data, "base64"); + const rawXDRBuffer = buffer.slice(3); + const rawXDRString = rawXDRBuffer.toString("base64"); + + const scVal = xdr.ScVal.fromXDR(rawXDRString, "base64"); + return Condition.fromScVal(scVal); +}; + +const operationSignatureToXDR = (args: { + ed25519Signature?: xdr.SorobanAuthorizationEntry; + utxoSignature?: OperationSignature; +}) => { + const { ed25519Signature, utxoSignature } = args; + + if (ed25519Signature !== undefined) { + return xdr.ScVal.scvVec([ + xdr.ScVal.scvBytes(ed25519Signature.toXDR("raw")), + ]).toXDR("base64"); + } else if (utxoSignature !== undefined) { + return xdr.ScVal.scvVec([ + nativeToScVal(utxoSignature.exp, { type: "i128" }), + xdr.ScVal.scvBytes(utxoSignature.sig), + ]).toXDR("base64"); + } else { + return xdr.ScVal.scvVec([]).toXDR("base64"); + } +}; + +const operationSignatureFromXDR = ( + data: string +): xdr.SorobanAuthorizationEntry | OperationSignature | undefined => { + const scVal = xdr.ScVal.fromXDR(data, "base64"); + if (scVal.switch().name !== xdr.ScValType.scvVec().name) { + throw new Error("Invalid ScVal type for Signature"); + } + + const vec = scVal.vec(); + + if (vec === null) { + throw new Error("Invalid ScVal vector for Signature"); + } + + if (vec.length === 0) { + return undefined; + } + + // ed25519 signature + if (vec.length === 1) { + const sigXDR = vec[0]; + if (sigXDR.switch().name !== xdr.ScValType.scvBytes().name) { + throw new Error("Invalid ScVal type for Ed25519 Signature"); + } + + return xdr.SorobanAuthorizationEntry.fromXDR(sigXDR.bytes(), "raw"); + } + + // UTXO signature + if (vec.length === 2) { + const expScVal = vec[0]; + const sigScVal = vec[1]; + + if (expScVal.switch().name !== xdr.ScValType.scvI128().name) { + throw new Error("Invalid ScVal type for UTXO Signature Expiration"); + } + if (sigScVal.switch().name !== xdr.ScValType.scvBytes().name) { + throw new Error("Invalid ScVal type for UTXO Signature"); + } + + const exp = Number(scValToBigInt(expScVal)); + const sig = sigScVal.bytes(); + + return { exp, sig }; + } +}; + +const operationToMLXDR = (operation: MoonlightOperationType): string => { + const rawScValXDR = operation.toXDR(); + + let typeByte: MLXDRTypeByte; + let signatureXDR = operationSignatureToXDR({}); + if (operation.isCreate()) { + typeByte = MLXDRTypeByte.CreateOperation; + } else if (operation.isWithdraw()) { + typeByte = MLXDRTypeByte.WithdrawOperation; + } else if (operation.isDeposit()) { + typeByte = MLXDRTypeByte.DepositOperation; + + if (operation.isSignedByEd25519()) { + signatureXDR = operationSignatureToXDR({ + ed25519Signature: operation.getEd25519Signature(), + }); + } + } else if (operation.isSpend()) { + typeByte = MLXDRTypeByte.SpendOperation; + if (operation.isSignedByUTXO()) { + signatureXDR = operationSignatureToXDR({ + utxoSignature: operation.getUTXOSignature(), + }); + } + } else { + throw new Error("Unknown operation type for MLXDR conversion"); + } + + const operationXDRWithSignature = xdr.ScVal.scvVec([ + xdr.ScVal.fromXDR(rawScValXDR, "base64"), + xdr.ScVal.fromXDR(signatureXDR, "base64"), + ]).toXDR("base64"); + + return appendMLXDRPrefixToRawXDR(operationXDRWithSignature, typeByte); +}; + +const MLXDRtoOperation = (data: string): MoonlightOperationType => { + if (!isOperation(data)) { + throw new Error("Data is not a valid MLXDR Operation"); + } + + const buffer = Buffer.from(data, "base64"); + const rawXDRBuffer = buffer.slice(3); + const rawXDRString = rawXDRBuffer.toString("base64"); + + const type = getXDRType(data); + if (type === null) { + throw new Error("Unable to determine MLXDR type for Operation"); + } + const scVal = xdr.ScVal.fromXDR(rawXDRString, "base64"); + + const vec = scVal.vec(); + + if (vec === null || vec.length < 1 || vec.length > 2) { + throw new Error("Invalid ScVal vector for operation"); + } + + const operationXDRScVal = vec[0]; + const signatureXDRScVal = vec.length === 2 ? vec[1] : undefined; + + if (type === MLXDRTypeByte.CreateOperation) { + return MoonlightOperation.fromScVal( + operationXDRScVal, + UTXOOperationType.CREATE + ); + } else if (type === MLXDRTypeByte.DepositOperation) { + if (signatureXDRScVal !== undefined) { + return MoonlightOperation.fromScVal( + operationXDRScVal, + UTXOOperationType.DEPOSIT + ).appendEd25519Signature( + operationSignatureFromXDR( + signatureXDRScVal.toXDR("base64") + ) as xdr.SorobanAuthorizationEntry + ); + } + return MoonlightOperation.fromScVal( + operationXDRScVal, + UTXOOperationType.DEPOSIT + ); + } else if (type === MLXDRTypeByte.WithdrawOperation) { + return MoonlightOperation.fromScVal( + operationXDRScVal, + UTXOOperationType.WITHDRAW + ); + } else if (type === MLXDRTypeByte.SpendOperation) { + if (signatureXDRScVal !== undefined) { + return MoonlightOperation.fromScVal( + operationXDRScVal, + UTXOOperationType.SPEND + ).appendUTXOSignature( + operationSignatureFromXDR( + signatureXDRScVal.toXDR("base64") + ) as OperationSignature + ); + } + return MoonlightOperation.fromScVal( + operationXDRScVal, + UTXOOperationType.SPEND + ); + } else { + throw new Error("Unknown MLXDR type for Operation"); + } +}; + +/** + * * MLXDR Module + * + * This module provides functions to work with Moonlight's custom XDR format (MLXDR). + * It includes utilities to check if data is in MLXDR format, identify the type of MLXDR data, + * and convert between standard Condition/Operation objects and their MLXDR representations. + * + * All MLXDR data is encoded as a BASE64 string prefixed with 'ML' to distinguish it from standard Stellar XDR. + * The first byte after the prefix indicates the object type (e.g., Create Condition, Deposit Operation, etc.). + * + * Example MLXDR encoding for a Spend Operation: + * - Decoded Bytes: + * - 0x30 0xb0: 'ML' Prefix + * - 0x05: Type Byte for Spend Operation + * - 0x...: Actual XDR Data + * + */ +export const MLXDR = { + is: isMLXDR, + isCondition, + isOperation, + isTransactionBundle, + getXDRType, + fromCondition: conditionToMLXDR, + toCondition: MLXDRtoCondition, + fromOperation: operationToMLXDR, + toOperation: MLXDRtoOperation, +}; diff --git a/src/custom-xdr/types.ts b/src/custom-xdr/types.ts new file mode 100644 index 0000000..5d9862a --- /dev/null +++ b/src/custom-xdr/types.ts @@ -0,0 +1,21 @@ +import { Buffer } from "buffer"; + +export enum MLXDRTypeByte { + CreateCondition = 0x01, + DepositCondition = 0x02, + WithdrawCondition = 0x03, + CreateOperation = 0x04, + SpendOperation = 0x05, + DepositOperation = 0x06, + WithdrawOperation = 0x07, + + TransactionBundle = 0x08, +} + +export const MLXDRPrefix: Buffer = Buffer.from([0x30, 0xb0]); + +export const MLXDRConditionBytes = [ + MLXDRTypeByte.CreateCondition, + MLXDRTypeByte.DepositCondition, + MLXDRTypeByte.WithdrawCondition, +]; diff --git a/src/derivation/base/index.ts b/src/derivation/base/index.ts index 4b4ac47..54a7847 100644 --- a/src/derivation/base/index.ts +++ b/src/derivation/base/index.ts @@ -1,5 +1,6 @@ +import { assert } from "../../utils/assert/index.ts"; import { deriveP256KeyPairFromSeed } from "../../utils/secp256r1/deriveP256KeyPairFromSeed.ts"; - +import * as E from "../error.ts"; /** * Generates a plain text seed by concatenating context, root, and index * @param context - The context where the seed will be used (e.g., network identifier) @@ -45,6 +46,70 @@ export class BaseDerivator< protected _context?: Context; protected _root?: Root; + //========================================== + // Meta Requirement Methods + //========================================== + + private requireNo(arg: "context" | "root"): void { + assert( + this[`_${arg}`] === undefined, + new E.PROPERTY_ALREADY_SET(arg, this[`_${arg}`] as string) + ); + } + + private requireNoContext(): void { + this.requireNo("context"); + } + + private requireNoRoot(): void { + this.requireNo("root"); + } + + /** + * Internal helper method to safely retrieve required properties. + * Uses method overloading to provide type-safe access to private fields. + * + * @param arg - The name of the property to retrieve + * @returns The value of the requested property + * @throws {Error} If the requested property is not set + * @private + */ + private require(arg: "context"): Context; + private require(arg: "root"): Root; + private require(arg: "context" | "root"): Context | Root { + if (this[`_${arg}`]) return this[`_${arg}`] as Context | Root; + throw new E.PROPERTY_NOT_SET(arg); + } + + //========================================== + // Getter Methods + //========================================== + + /** + * Returns the derivation context (e.g., network identifier) + * + * @returns The derivation context + * @throws {Error} If the context is not set (should never happen with factory methods) + * + * @example + * ```typescript + * const context = derivator.getContext(); + * ``` + */ + public getContext(): Context { + return this.require("context"); + } + + private getRoot(): Root { + return this.require("root"); + } + + public isSet(arg: "context" | "root"): boolean { + return this[`_${arg}`] !== undefined; + } + + //======== + /** * Sets the derivation context for this derivator * @@ -57,9 +122,8 @@ export class BaseDerivator< * ``` */ withContext(context: Context): this { - if (this._context !== undefined) { - throw Error("Context has already been set"); - } + this.requireNoContext(); + this._context = context; return this; } @@ -76,9 +140,8 @@ export class BaseDerivator< * ``` */ withRoot(root: Root): this { - if (this._root !== undefined) { - throw Error("Root has already been set"); - } + this.requireNoRoot(); + this._root = root; return this; } @@ -97,9 +160,7 @@ export class BaseDerivator< * ``` */ assembleSeed(index: Index): `${Context}${Root}${Index}` { - this.assertConfigured(); - - return generatePlainTextSeed(this._context!, this._root!, index); + return generatePlainTextSeed(this.getContext(), this.getRoot(), index); } /** @@ -114,8 +175,6 @@ export class BaseDerivator< * ``` */ async hashSeed(index: Index): Promise { - this.assertConfigured(); - const seed = this.assembleSeed(index); return await hashSeed(seed); } @@ -138,30 +197,11 @@ export class BaseDerivator< async deriveKeypair( index: Index ): Promise<{ publicKey: Uint8Array; privateKey: Uint8Array }> { - this.assertConfigured(); - const seed = this.assembleSeed(index); const hashedSeed = await hashSeed(seed); return deriveP256KeyPairFromSeed(hashedSeed); } - /** - * Gets the current context value - * - * @returns The current context or undefined if not set - * - * @example - * ```typescript - * const context = derivator.getContext(); - * ``` - */ - getContext(): Context { - if (this._context === undefined) { - throw Error("Context is required but has not been set"); - } - return this._context; - } - /** * Checks if the derivator is fully configured and ready to derive keypairs * @@ -175,19 +215,6 @@ export class BaseDerivator< * ``` */ isConfigured(): boolean { - return !!this._context && !!this._root; - } - - /** - * Checks if the derivator is fully configured and throws an error if not - * - * @throws Error if the derivator is not properly configured - */ - assertConfigured(): void { - if (!this.isConfigured()) { - throw new Error( - "Derivator is not properly configured: missing context or root" - ); - } + return this.isSet("context") && this.isSet("root"); } } diff --git a/src/derivation/base/index.unit.test.ts b/src/derivation/base/index.unit.test.ts index 524b52b..eca4561 100644 --- a/src/derivation/base/index.unit.test.ts +++ b/src/derivation/base/index.unit.test.ts @@ -1,5 +1,6 @@ import { BaseDerivator, generatePlainTextSeed, hashSeed } from "./index.ts"; import { assertEquals, assertThrows, assertExists } from "@std/assert"; +import * as E from "../error.ts"; Deno.test("BaseDerivator", async (t) => { await t.step("assembleSeed should work with complete components", () => { @@ -18,8 +19,7 @@ Deno.test("BaseDerivator", async (t) => { assertThrows( () => derivator.assembleSeed("test-index"), - Error, - "Derivator is not properly configured" + E.PROPERTY_NOT_SET ); }); @@ -30,8 +30,7 @@ Deno.test("BaseDerivator", async (t) => { assertThrows( () => derivator.assembleSeed("test-index"), - Error, - "Derivator is not properly configured" + E.PROPERTY_NOT_SET ); }); @@ -67,8 +66,7 @@ Deno.test("BaseDerivator", async (t) => { assertThrows( () => derivator.withContext("another-context"), - Error, - "Context has already been set" + E.PROPERTY_ALREADY_SET ); }); @@ -79,8 +77,7 @@ Deno.test("BaseDerivator", async (t) => { assertThrows( () => derivator.withRoot("another-root"), - Error, - "Root has already been set" + E.PROPERTY_ALREADY_SET ); }); }); diff --git a/src/derivation/error.ts b/src/derivation/error.ts new file mode 100644 index 0000000..7b5f479 --- /dev/null +++ b/src/derivation/error.ts @@ -0,0 +1,86 @@ +import { MoonlightError } from "../error/index.ts"; + +export type Meta = { + cause: Error | null; + data: unknown; +}; + +export type DerivationErrorShape = { + code: Code; + message: string; + details: string; + cause?: Error; + data: unknown; +}; + +export abstract class DerivationError< + Code extends string +> extends MoonlightError { + override readonly meta: Meta; + + constructor(args: DerivationErrorShape) { + const meta = { + cause: args.cause || null, + data: args.data, + }; + + super({ + domain: "derivation" as const, + source: "@Moonlight/derivation", + code: args.code, + message: args.message, + details: args.details, + meta, + }); + + this.meta = meta; + } +} + +export enum Code { + UNEXPECTED_ERROR = "DER_000", + PROPERTY_ALREADY_SET = "DER_001", + PROPERTY_NOT_SET = "DER_002", +} + +// Currently unused, reserving +// +// export class UNEXPECTED_ERROR extends ContractError { +// constructor(cause: Error) { +// super({ +// code: Code.UNEXPECTED_ERROR, +// message: "An unexpected error occurred in the Contract module!", +// details: "See the 'cause' for more details", +// cause, +// data: {}, +// }); +// } +// } + +export class PROPERTY_ALREADY_SET extends DerivationError { + constructor(property: string, value: string) { + super({ + code: Code.PROPERTY_ALREADY_SET, + message: `Property '${property}' is already set as: ${value}`, + details: `The property '${property}' has already been set for this derivator. Once set, this property cannot be modified.`, + data: { property, value }, + }); + } +} + +export class PROPERTY_NOT_SET extends DerivationError { + constructor(property: string) { + super({ + code: Code.PROPERTY_NOT_SET, + message: `Property '${property}' is not set`, + details: `The property '${property}' must be set before it can be accessed. Please ensure that you have configured this property appropriately before attempting to use it.`, + data: { property }, + }); + } +} + +export const DER_ERRORS = { + // [Code.UNEXPECTED_ERROR]: UNEXPECTED_ERROR, + [Code.PROPERTY_ALREADY_SET]: PROPERTY_ALREADY_SET, + [Code.PROPERTY_NOT_SET]: PROPERTY_NOT_SET, +}; diff --git a/src/error/errors.ts b/src/error/errors.ts new file mode 100644 index 0000000..009aeb4 --- /dev/null +++ b/src/error/errors.ts @@ -0,0 +1,16 @@ +import { GEN_ERRORS } from "./index.ts"; +import { DER_ERRORS } from "../derivation/error.ts"; +import { UKP_ERRORS } from "../core/utxo-keypair/error.ts"; +import { OPR_ERRORS } from "../operation/error.ts"; +import { PCH_ERRORS } from "../privacy-channel/error.ts"; +import { TBU_ERRORS } from "../transaction-builder/error.ts"; +import { UBA_ERRORS } from "../utxo-based-account/error.ts"; +export const ERRORS = { + ...GEN_ERRORS, + ...DER_ERRORS, + ...UKP_ERRORS, + ...OPR_ERRORS, + ...PCH_ERRORS, + ...TBU_ERRORS, + ...UBA_ERRORS, +}; diff --git a/src/error/index.ts b/src/error/index.ts new file mode 100644 index 0000000..e218574 --- /dev/null +++ b/src/error/index.ts @@ -0,0 +1,143 @@ +import type { + BaseMeta, + MoonlightErrorShape, + Diagnostic, + ErrorDomain, +} from "./types.ts"; + +/** + * MoonlightError - Custom error class for Moonlight SDK + * Extends the native Error class with additional properties and methods + */ +export class MoonlightError< + C extends string = string, + M extends BaseMeta = BaseMeta +> extends Error { + readonly domain: ErrorDomain; + readonly code: C; + readonly source: string; + readonly details?: string; + readonly diagnostic?: Diagnostic; + readonly meta?: M; + + /** + * + * @description Constructs a new MoonlightError instance. + * + * @param {MoonlightErrorShape} e - The error shape containing all necessary properties. + */ + constructor(e: MoonlightErrorShape) { + super(e.message); + this.name = "MoonlightError " + e.code; + this.domain = e.domain; + this.code = e.code; + this.source = e.source; + this.details = e.details; + this.diagnostic = e.diagnostic; + this.meta = e.meta; + } + + /** + * + * @description Serializes the MoonlightError to a JSON-compatible object. + * + * @returns {Record} A JSON-compatible representation of the error. + */ + toJSON(): Record { + return { + name: this.name, + domain: this.domain, + code: this.code, + message: this.message, + source: this.source, + details: this.details, + diagnostic: this.diagnostic, + meta: this.meta, + }; + } + + /** + * + * @description Type guard to check if an unknown value is a MoonlightError. + * + * @param {unknown} e - The value to check. + * + * @returns {boolean} True if the value is a MoonlightError, false otherwise. + */ + static is(e: unknown): e is MoonlightError { + return e instanceof MoonlightError; + } + + /** + * + * @description Creates a generic unexpected error instance. + * + * @param {object} args - Optional parameters to customize the error + * @param {ErrorDomain} [domain="general"] - The error domain + * @param {string} [source="moonlight"] - The source of the error + * @param {string} [code="GEN_000"] - The error code + * @param {string} [message="Unexpected error"] - The error message + * @param {string} [details="An unexpected error occurred"] - Additional details about the error + * @param {BaseMeta} [meta] - Additional metadata for the error + * @param {unknown} [cause] - The underlying cause of the error + * + * @returns A new instance of MoonlightError + * + */ + static unexpected(args?: { + domain?: ErrorDomain; + source?: string; + code?: string; + message?: string; + details?: string; + meta?: BaseMeta; + cause?: unknown; + }): MoonlightError { + return new MoonlightError({ + domain: args?.domain ?? "general", + source: args?.source ?? "@Moonlight", + code: args?.code ?? GeneralErrorCode.UNEXPECTED_ERROR, + message: args?.message ?? "Unexpected error", + details: args?.details ?? "An unexpected error occurred", + meta: { ...args?.meta, cause: args?.cause }, + }); + } + + /** + * + * @description Creates a MoonlightError from an unknown error. + * + * @param {unknown} error - The unknown error to convert + * @param {Partial>} ctx - Optional context to include in the error + * + * @returns {MoonlightError} A new instance of MoonlightError + */ + static fromUnknown( + error: unknown, + ctx?: Partial> + ): MoonlightError { + if (error instanceof MoonlightError) return error; + if (error instanceof Error) { + return new MoonlightError({ + domain: ctx?.domain ?? "general", + source: ctx?.source ?? "@Moonlight", + code: ctx?.code ?? GeneralErrorCode.UNKNOWN_ERROR, + message: error.message, + details: ctx?.details ?? error.stack, + diagnostic: ctx?.diagnostic, + meta: { ...ctx?.meta, cause: error }, + }); + } + return MoonlightError.unexpected({ cause: error, ...ctx }); + } +} + +export enum GeneralErrorCode { + UNEXPECTED_ERROR = "GEN_000", + UNKNOWN_ERROR = "GEN_001", +} + +export const GEN_ERRORS = { + [GeneralErrorCode.UNEXPECTED_ERROR]: MoonlightError, + [GeneralErrorCode.UNKNOWN_ERROR]: MoonlightError, +}; diff --git a/src/error/index.unit.test.ts b/src/error/index.unit.test.ts new file mode 100644 index 0000000..8d4779b --- /dev/null +++ b/src/error/index.unit.test.ts @@ -0,0 +1,184 @@ +import { + assert, + assertEquals, + assertObjectMatch, + assertStrictEquals, +} from "@std/assert"; +import { MoonlightError, GeneralErrorCode } from "./index.ts"; +import type { BaseMeta, Diagnostic, MoonlightErrorShape } from "./types.ts"; + +Deno.test("MoonlightError", async (t) => { + await t.step("constructor should set all fields correctly", () => { + const diagnostic: Diagnostic = { + rootCause: "bad format", + suggestion: "use a valid G... public key", + }; + const meta: BaseMeta = { data: { accountId: "GABC" } }; + const shape: MoonlightErrorShape<"DER_001", BaseMeta> = { + domain: "derivation", + source: "@moonlight/sdk", + code: "DER_001", + message: "Invalid derivation key", + details: "malformed key", + diagnostic, + meta, + }; + + const e = new MoonlightError(shape); + + assert(e instanceof Error); + assert(e instanceof MoonlightError); + assertStrictEquals(e.name, "MoonlightError DER_001"); + assertStrictEquals(e.domain, "derivation"); + assertStrictEquals(e.source, "@moonlight/sdk"); + assertStrictEquals(e.code, "DER_001"); + assertStrictEquals(e.message, "Invalid derivation key"); + assertStrictEquals(e.details, "malformed key"); + assertObjectMatch(e.diagnostic!, diagnostic); + assertObjectMatch(e.meta!, meta); + }); + + await t.step("toJSON should return plain snapshot of all fields", () => { + const e = new MoonlightError({ + domain: "general", + source: "@Moonlight", + code: GeneralErrorCode.UNEXPECTED_ERROR, + message: "Unexpected error", + details: "boom", + meta: { data: { x: 1 } }, + diagnostic: { + rootCause: "root", + suggestion: "fix", + }, + }); + + const j = e.toJSON(); + assertEquals(j.name, "MoonlightError GEN_000"); + assertEquals(j.domain, "general"); + assertEquals(j.code, GeneralErrorCode.UNEXPECTED_ERROR); + assertEquals(j.message, "Unexpected error"); + assertEquals(j.source, "@Moonlight"); + assertEquals(j.details, "boom"); + assertObjectMatch(j.meta!, { data: { x: 1 } }); + assertObjectMatch(j.diagnostic!, { + rootCause: "root", + suggestion: "fix", + }); + }); + + await t.step("is should detect MoonlightError instances correctly", () => { + const e = new MoonlightError({ + domain: "general", + source: "moonlight", + code: GeneralErrorCode.UNEXPECTED_ERROR, + message: "test message", + }); + assert(MoonlightError.is(e)); + assert(!MoonlightError.is(new Error("x"))); + assert(!MoonlightError.is({})); + assert(!MoonlightError.is(null)); + assert(!MoonlightError.is(undefined)); + }); + + await t.step( + "unexpected should build general error and preserve cause", + () => { + const cause = new Error("disk not found"); + const e = MoonlightError.unexpected({ + message: "fail", + details: "ctx", + source: "@moonlight/test", + meta: { data: { id: 7 } }, + cause, + }); + + assert(e instanceof MoonlightError); + assertEquals(e.domain, "general"); + assertEquals(e.source, "@moonlight/test"); + assertEquals(e.code, GeneralErrorCode.UNEXPECTED_ERROR); + assertEquals(e.message, "fail"); + assertEquals(e.details, "ctx"); + assertStrictEquals(e.meta?.cause, cause); + assertObjectMatch(e.meta!.data as Record, { id: 7 }); + } + ); + + await t.step( + "unexpected should return error with defaults when no args provided", + () => { + const out = MoonlightError.unexpected(); + assert(out instanceof MoonlightError); + assertEquals(out.domain, "general"); + assertEquals(out.source, "@Moonlight"); + assertEquals(out.code, GeneralErrorCode.UNEXPECTED_ERROR); + assertEquals(out.message, "Unexpected error"); + assertStrictEquals(out.details, "An unexpected error occurred"); + } + ); + + await t.step( + "fromUnknown should return same instance for MoonlightError", + () => { + const original = new MoonlightError({ + domain: "derivation", + source: "@moonlight/sdk", + code: "DER_001", + message: "derivation failed", + }); + const out = MoonlightError.fromUnknown(original); + assertStrictEquals(out, original); + } + ); + + await t.step( + "fromUnknown should wrap native Error with stack in details", + () => { + const error = new Error("mock error"); + + const out = MoonlightError.fromUnknown(error); + assert(out instanceof MoonlightError); + assertEquals(out.domain, "general"); + assertEquals(out.source, "@Moonlight"); + assertEquals(out.code, GeneralErrorCode.UNKNOWN_ERROR); + assertEquals(out.message, "mock error"); + assertStrictEquals(out.details, error.stack); + assert(typeof out.details === "string"); + assertStrictEquals(out.meta?.cause, error); + } + ); + + await t.step( + "fromUnknown should wrap native Error with custom context", + () => { + const native = new Error("boom"); + const wrapped = MoonlightError.fromUnknown(native, { + domain: "derivation", + source: "@moonlight/sdk", + code: "DER_999", + }); + + assert(wrapped instanceof MoonlightError); + assertEquals(wrapped.domain, "derivation"); + assertEquals(wrapped.source, "@moonlight/sdk"); + assertEquals(wrapped.code, "DER_999"); + assertEquals(wrapped.message, "boom"); + assert(typeof wrapped.details === "string"); + assertStrictEquals(wrapped.meta?.cause, native); + } + ); + + await t.step("fromUnknown should use unexpected for non-error values", () => { + const wrapped = MoonlightError.fromUnknown(42, { + domain: "general", + source: "@moonlight/sdk", + message: "bad input", + }); + + assert(wrapped instanceof MoonlightError); + assertEquals(wrapped.domain, "general"); + assertEquals(wrapped.source, "@moonlight/sdk"); + assertEquals(wrapped.code, GeneralErrorCode.UNEXPECTED_ERROR); + assertEquals(wrapped.message, "bad input"); + assertStrictEquals(wrapped.meta?.cause, 42); + }); +}); diff --git a/src/error/types.ts b/src/error/types.ts new file mode 100644 index 0000000..231d06f --- /dev/null +++ b/src/error/types.ts @@ -0,0 +1,32 @@ +export type ErrorDomain = + | "derivation" + | "general" + | "utxo-keypair" + | "operation" + | "privacy-channel" + | "transaction-builder" + | "utxo-based-account"; + +export type BaseMeta = { + cause?: unknown; // chained errors + data?: unknown; // domain-specific payload +}; + +export interface MoonlightErrorShape< + Code extends string, + Meta extends BaseMeta +> { + domain: ErrorDomain; + code: Code; // ex: "CC_001" + message: string; + source: string; // ex: "@Moonlight-sdk/core" + details?: string; + diagnostic?: Diagnostic; + meta?: Meta; +} + +export type Diagnostic = { + rootCause: string; + suggestion: string; + materials?: string[]; +}; diff --git a/src/operation/error.ts b/src/operation/error.ts new file mode 100644 index 0000000..badddd2 --- /dev/null +++ b/src/operation/error.ts @@ -0,0 +1,281 @@ +import type { Ed25519PublicKey } from "@colibri/core"; +import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; +import { MoonlightError } from "../error/index.ts"; + +export type Meta = { + cause: Error | null; + data: unknown; +}; + +export type OperationErrorShape = { + code: Code; + message: string; + details: string; + cause?: Error; + data: unknown; +}; + +export enum Code { + UNEXPECTED_ERROR = "OPR_000", + PROPERTY_NOT_SET = "OPR_001", + AMOUNT_TOO_LOW = "OPR_002", + INVALID_ED25519_PK = "OPR_003", + + CANNOT_CONVERT_SPEND_OP = "OPR_004", + UNSUPPORTED_OP_TYPE_FOR_SCVAL_CONVERSION = "OPR_005", + OP_IS_NOT_CREATE = "OPR_006", + OP_IS_NOT_SPEND = "OPR_007", + OP_IS_NOT_DEPOSIT = "OPR_008", + OP_IS_NOT_WITHDRAW = "OPR_009", + OP_IS_NOT_SIGNABLE = "OPR_010", + OP_HAS_NO_CONDITIONS = "OPR_011", + SIGNER_IS_NOT_DEPOSITOR = "OPR_012", + OP_ALREADY_SIGNED = "OPR_013", + INVALID_SCVAL_TYPE_FOR_OPERATION = "OPR_014", + INVALID_SCVAL_VEC_FOR_OPERATION = "OPR_015", + + INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION = "OPR_016", + INVALID_SCVAL_VEC_FOR_CONDITIONS = "OPR_017", + INVALID_SCVAL_VEC_FOR_CONDITION = "OPR_018", +} + +export abstract class OperationError extends MoonlightError { + override readonly meta: Meta; + + constructor(args: OperationErrorShape) { + const meta = { + cause: args.cause || null, + data: args.data, + }; + + super({ + domain: "operation" as const, + source: "@Moonlight/operation", + code: args.code, + message: args.message, + details: args.details, + meta, + }); + + this.meta = meta; + } +} + +export class PROPERTY_NOT_SET extends OperationError { + constructor(property: string) { + super({ + code: Code.PROPERTY_NOT_SET, + message: `Property not set: ${property}`, + details: `The required property ${property} is not set in the operation.`, + data: { property }, + }); + } +} + +export class AMOUNT_TOO_LOW extends OperationError { + constructor(amount: bigint) { + super({ + code: Code.AMOUNT_TOO_LOW, + message: `Amount too low: ${amount}`, + details: `The provided amount ${amount} is below the minimum required. It must be greater than zero.`, + data: { amount: `${amount}` }, + }); + } +} + +export class INVALID_ED25519_PK extends OperationError { + constructor(publicKey: string) { + super({ + code: Code.INVALID_ED25519_PK, + message: `Invalid Ed25519 public key: ${publicKey}`, + details: `The provided public key ${publicKey} is not a valid Stellar Ed25519 key. It must follow the strkey standard.`, + data: { publicKey: `${publicKey}` }, + }); + } +} + +export class CANNOT_CONVERT_SPEND_OP extends OperationError { + constructor(utxoPublicKey: UTXOPublicKey) { + super({ + code: Code.CANNOT_CONVERT_SPEND_OP, + message: `Cannot convert spend operation to condition.`, + details: `The conversion of operation to condition failed because the operation is of type SPEND. This type cannot be used as a condition.`, + data: { utxoPublicKey }, + }); + } +} + +export class UNSUPPORTED_OP_TYPE_FOR_SCVAL_CONVERSION extends OperationError { + constructor(opType: string) { + super({ + code: Code.UNSUPPORTED_OP_TYPE_FOR_SCVAL_CONVERSION, + message: `Unsupported operation type for SCVal conversion: ${opType}`, + details: `The operation type ${opType} is not supported for SCVal conversion. Only DEPOSIT, WITHDRAW, CREATE and SPEND types are supported.`, + data: { opType }, + }); + } +} + +export class OP_IS_NOT_CREATE extends OperationError { + constructor(opType: string) { + super({ + code: Code.OP_IS_NOT_CREATE, + message: `Operation is not of type CREATE`, + details: `The current operation could not be converted to ScVal as a CREATE operation because the type doesn't match.`, + data: { opType }, + }); + } +} + +export class OP_IS_NOT_SPEND extends OperationError { + constructor(opType: string) { + super({ + code: Code.OP_IS_NOT_SPEND, + message: `Operation is not of type SPEND`, + details: `The current operation could not be converted to ScVal as a SPEND operation because the type doesn't match.`, + data: { opType }, + }); + } +} + +export class OP_IS_NOT_DEPOSIT extends OperationError { + constructor(opType: string) { + super({ + code: Code.OP_IS_NOT_DEPOSIT, + message: `Operation is not of type DEPOSIT`, + details: `The current operation could not be converted to ScVal as a DEPOSIT operation because the type doesn't match.`, + data: { opType }, + }); + } +} +export class OP_IS_NOT_WITHDRAW extends OperationError { + constructor(opType: string) { + super({ + code: Code.OP_IS_NOT_WITHDRAW, + message: `Operation is not of type WITHDRAW`, + details: `The current operation could not be converted to ScVal as a WITHDRAW operation because the type doesn't match.`, + data: { opType }, + }); + } +} + +export class OP_IS_NOT_SIGNABLE extends OperationError { + constructor(opType: string, signType: string) { + super({ + code: Code.OP_IS_NOT_SIGNABLE, + message: `Operation of type ${opType} is not signable with ${signType}`, + details: `The operation type ${opType} does not support signing with ${signType}.`, + data: { opType }, + }); + } +} + +export class OP_HAS_NO_CONDITIONS extends OperationError { + constructor(utxoPublicKey: UTXOPublicKey) { + super({ + code: Code.OP_HAS_NO_CONDITIONS, + message: `The SPEND operation for UTXO ${utxoPublicKey} has no conditions.`, + details: `The operation of UTXO ${utxoPublicKey} cannot be signed because it has no conditions.`, + data: { utxoPublicKey }, + }); + } +} + +export class SIGNER_IS_NOT_DEPOSITOR extends OperationError { + constructor(signerPk: string, depositorPk: string) { + super({ + code: Code.SIGNER_IS_NOT_DEPOSITOR, + message: `Signer public key ${signerPk} does not match depositor public key ${depositorPk}.`, + details: `The operation cannot be signed because the signer's public key does not match the depositor's public key defined in the operation.`, + data: { signerPk, depositorPk }, + }); + } +} + +export class OP_ALREADY_SIGNED extends OperationError { + constructor(signatureType: string) { + super({ + code: Code.OP_ALREADY_SIGNED, + message: `Operation is already signed.`, + details: `The operation cannot be signed again because it is already signed with ${signatureType}.`, + data: { signatureType }, + }); + } +} + +export class INVALID_SCVAL_TYPE_FOR_OPERATION extends OperationError { + constructor(expectedType: string, actualType: string) { + super({ + code: Code.INVALID_SCVAL_TYPE_FOR_OPERATION, + message: `Invalid SCVal type for operation conversion.`, + details: `The SCVal type ${actualType} is not valid for conversion to the expected operation type ${expectedType}.`, + data: { expectedType, actualType }, + }); + } +} + +export class INVALID_SCVAL_VEC_FOR_OPERATION extends OperationError { + constructor() { + super({ + code: Code.INVALID_SCVAL_VEC_FOR_OPERATION, + message: `Invalid SCVal vector type for operation conversion.`, + details: `The SCVal type is null is not a vector type required for operation conversion.`, + data: {}, + }); + } +} + +export class INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION extends OperationError { + constructor(opType: string, expectedLength: number, actualLength: number) { + super({ + code: Code.INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION, + message: `Invalid SCVal vector length for operation type ${opType}.`, + details: `The SCVal vector length ${actualLength} does not match the expected length ${expectedLength} for operation type ${opType}.`, + data: { opType, expectedLength, actualLength }, + }); + } +} + +export class INVALID_SCVAL_VEC_FOR_CONDITIONS extends OperationError { + constructor(utxoOrPk: UTXOPublicKey | Ed25519PublicKey) { + super({ + code: Code.INVALID_SCVAL_VEC_FOR_CONDITIONS, + message: `Invalid SCVal vector for conditions in op with address ${utxoOrPk}.`, + details: `The SCVal vector for conditions in op with address ${utxoOrPk} is not valid.`, + data: { utxoOrPk }, + }); + } +} + +export class INVALID_SCVAL_VEC_FOR_CONDITION extends OperationError { + constructor(utxoOrPk: UTXOPublicKey | Ed25519PublicKey, type: string) { + super({ + code: Code.INVALID_SCVAL_VEC_FOR_CONDITION, + message: `Invalid SCVal vector for condition in op with address ${utxoOrPk}.`, + details: `The SCVal vector for a condition in op with address ${utxoOrPk} is not valid. A type of ${type} was provided instead of an ScVec.`, + data: { utxoOrPk, type }, + }); + } +} + +export const OPR_ERRORS = { + [Code.AMOUNT_TOO_LOW]: AMOUNT_TOO_LOW, + [Code.INVALID_ED25519_PK]: INVALID_ED25519_PK, + [Code.PROPERTY_NOT_SET]: PROPERTY_NOT_SET, + [Code.CANNOT_CONVERT_SPEND_OP]: CANNOT_CONVERT_SPEND_OP, + [Code.UNSUPPORTED_OP_TYPE_FOR_SCVAL_CONVERSION]: + UNSUPPORTED_OP_TYPE_FOR_SCVAL_CONVERSION, + [Code.OP_IS_NOT_CREATE]: OP_IS_NOT_CREATE, + [Code.OP_IS_NOT_SPEND]: OP_IS_NOT_SPEND, + [Code.OP_IS_NOT_DEPOSIT]: OP_IS_NOT_DEPOSIT, + [Code.OP_IS_NOT_WITHDRAW]: OP_IS_NOT_WITHDRAW, + [Code.OP_IS_NOT_SIGNABLE]: OP_IS_NOT_SIGNABLE, + [Code.OP_HAS_NO_CONDITIONS]: OP_HAS_NO_CONDITIONS, + [Code.SIGNER_IS_NOT_DEPOSITOR]: SIGNER_IS_NOT_DEPOSITOR, + [Code.OP_ALREADY_SIGNED]: OP_ALREADY_SIGNED, + [Code.INVALID_SCVAL_TYPE_FOR_OPERATION]: INVALID_SCVAL_TYPE_FOR_OPERATION, + [Code.INVALID_SCVAL_VEC_FOR_OPERATION]: INVALID_SCVAL_VEC_FOR_OPERATION, + [Code.INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION]: + INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION, + [Code.INVALID_SCVAL_VEC_FOR_CONDITION]: INVALID_SCVAL_VEC_FOR_CONDITION, +}; diff --git a/src/operation/index.ts b/src/operation/index.ts index 3e575f4..daaefff 100644 --- a/src/operation/index.ts +++ b/src/operation/index.ts @@ -1,18 +1,41 @@ -import { type Ed25519PublicKey, StrKey } from "@colibri/core"; +import { + type ContractId, + type Ed25519PublicKey, + isTransactionSigner, + StrKey, + type TransactionSigner, +} from "@colibri/core"; import type { Condition as ConditionType } from "../conditions/types.ts"; import { UTXOOperationType } from "./types.ts"; import type { BaseOperation, CreateOperation, DepositOperation, + OperationSignature, SpendOperation, WithdrawOperation, } from "./types.ts"; import { Condition } from "../conditions/index.ts"; -import { nativeToScVal, xdr } from "@stellar/stellar-sdk"; +import { + authorizeEntry, + type Keypair, + nativeToScVal, + scValToBigInt, + scValToNative, + xdr, +} from "@stellar/stellar-sdk"; import { Buffer } from "node:buffer"; -import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; +import type { + IUTXOKeypairBase, + UTXOPublicKey, +} from "../core/utxo-keypair-base/types.ts"; +import * as E from "./error.ts"; +import { assert } from "../utils/assert/assert.ts"; +import { buildAuthPayloadHash } from "../utils/auth/build-auth-payload.ts"; +import { generateNonce } from "../utils/common/index.ts"; +import { buildDepositAuthEntry } from "../utils/auth/deposit-auth-entry copy.ts"; +import { MLXDR } from "../custom-xdr/index.ts"; export class MoonlightOperation implements BaseOperation { private _op: UTXOOperationType; @@ -21,6 +44,9 @@ export class MoonlightOperation implements BaseOperation { private _utxo?: UTXOPublicKey; private _conditions?: ConditionType[]; + private _utxoSignature?: OperationSignature; + private _ed25519Signature?: xdr.SorobanAuthorizationEntry; + private constructor({ op, amount, @@ -32,10 +58,6 @@ export class MoonlightOperation implements BaseOperation { publicKey?: Ed25519PublicKey; utxo?: UTXOPublicKey; }) { - if (amount !== undefined && amount <= 0n) { - throw new Error("Amount must be greater than zero"); - } - // Business rule: CREATE operations cannot have conditions. // This is because conditions are only applicable to DEPOSIT, SPEND, and WITHDRAW operations. // Attempting to add conditions to CREATE would violate the intended operation semantics. @@ -48,6 +70,8 @@ export class MoonlightOperation implements BaseOperation { } static create(utxo: UTXOPublicKey, amount: bigint): CreateOperation { + assert(amount > 0n, new E.AMOUNT_TOO_LOW(amount)); + return new MoonlightOperation({ op: UTXOOperationType.CREATE, utxo, @@ -59,9 +83,12 @@ export class MoonlightOperation implements BaseOperation { publicKey: Ed25519PublicKey, amount: bigint ): DepositOperation { - if (!StrKey.isValidEd25519PublicKey(publicKey)) { - throw new Error("Invalid Ed25519 public key"); - } + assert(amount > 0n, new E.AMOUNT_TOO_LOW(amount)); + + assert( + StrKey.isValidEd25519PublicKey(publicKey), + new E.INVALID_ED25519_PK(publicKey) + ); return new MoonlightOperation({ op: UTXOOperationType.DEPOSIT, @@ -74,9 +101,13 @@ export class MoonlightOperation implements BaseOperation { publicKey: Ed25519PublicKey, amount: bigint ): WithdrawOperation { - if (!StrKey.isValidEd25519PublicKey(publicKey)) { - throw new Error("Invalid Ed25519 public key."); - } + assert(amount > 0n, new E.AMOUNT_TOO_LOW(amount)); + + assert( + StrKey.isValidEd25519PublicKey(publicKey), + new E.INVALID_ED25519_PK(publicKey) + ); + return new MoonlightOperation({ op: UTXOOperationType.WITHDRAW, publicKey, @@ -91,6 +122,142 @@ export class MoonlightOperation implements BaseOperation { }) as SpendOperation; } + static fromXDR( + xdrString: string, + type: UTXOOperationType + ): CreateOperation | DepositOperation | WithdrawOperation | SpendOperation { + const scVal = xdr.ScVal.fromXDR(xdrString, "base64"); + + if (type === UTXOOperationType.SPEND) { + return this.fromScVal(scVal, UTXOOperationType.SPEND); + } + if (type === UTXOOperationType.DEPOSIT) { + return this.fromScVal(scVal, UTXOOperationType.DEPOSIT); + } + if (type === UTXOOperationType.WITHDRAW) { + return this.fromScVal(scVal, UTXOOperationType.WITHDRAW); + } + if (type === UTXOOperationType.CREATE) { + return this.fromScVal(scVal, UTXOOperationType.CREATE); + } + + throw new E.UNSUPPORTED_OP_TYPE_FOR_SCVAL_CONVERSION(type); + } + + static fromScVal( + scVal: xdr.ScVal, + type: UTXOOperationType.CREATE + ): CreateOperation; + static fromScVal( + scVal: xdr.ScVal, + type: UTXOOperationType.DEPOSIT + ): DepositOperation; + static fromScVal( + scVal: xdr.ScVal, + type: UTXOOperationType.WITHDRAW + ): WithdrawOperation; + static fromScVal( + scVal: xdr.ScVal, + type: UTXOOperationType.SPEND + ): SpendOperation; + public static fromScVal( + scVal: xdr.ScVal, + type: UTXOOperationType + ): CreateOperation | DepositOperation | WithdrawOperation | SpendOperation { + assert( + scVal.switch().name === xdr.ScValType.scvVec().name, + new E.INVALID_SCVAL_TYPE_FOR_OPERATION( + xdr.ScValType.scvVec().name, + scVal.switch().name + ) + ); + + const vec = scVal.vec(); + + assert(vec !== null, new E.INVALID_SCVAL_VEC_FOR_OPERATION()); + + if (type === UTXOOperationType.CREATE) { + assert( + vec.length === 2, + new E.INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION(type, 2, vec.length) + ); + const utxo: UTXOPublicKey = Uint8Array.from(vec[0].bytes()); + const amount = scValToBigInt(vec[1]); + return this.create(utxo, amount); + } + if (type === UTXOOperationType.SPEND) { + assert( + vec.length === 2, + new E.INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION(type, 2, vec.length) + ); + const utxo: UTXOPublicKey = Uint8Array.from(vec[0].bytes()); + const conditionsScVal = vec[1].vec(); + + assert( + conditionsScVal !== null, + new E.INVALID_SCVAL_VEC_FOR_CONDITIONS(utxo) + ); + + const conditions: ConditionType[] = conditionsScVal.map((cScVal) => { + assert( + cScVal.switch().name === xdr.ScValType.scvVec().name, + new E.INVALID_SCVAL_VEC_FOR_CONDITION(utxo, cScVal.switch().name) + ); + + return Condition.fromScVal(cScVal); + }); + return this.spend(utxo).addConditions(conditions); + } + if (type === UTXOOperationType.DEPOSIT) { + assert( + vec.length === 3, + new E.INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION(type, 3, vec.length) + ); + const publicKey = scValToNative(vec[0]) as Ed25519PublicKey; + const amount = scValToBigInt(vec[1]); + const conditionsScVal = vec[2].vec(); + + assert( + conditionsScVal !== null, + new E.INVALID_SCVAL_VEC_FOR_CONDITIONS(publicKey) + ); + + const conditions: ConditionType[] = conditionsScVal.map((cScVal) => { + assert( + cScVal.switch().name === xdr.ScValType.scvVec().name, + new E.INVALID_SCVAL_VEC_FOR_CONDITION(publicKey, cScVal.switch().name) + ); + return Condition.fromScVal(cScVal); + }); + return this.deposit(publicKey, amount).addConditions(conditions); + } + if (type === UTXOOperationType.WITHDRAW) { + assert( + vec.length === 3, + new E.INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION(type, 3, vec.length) + ); + const publicKey = scValToNative(vec[0]) as Ed25519PublicKey; + const amount = scValToBigInt(vec[1]); + const conditionsScVal = vec[2].vec(); + + assert( + conditionsScVal !== null, + new E.INVALID_SCVAL_VEC_FOR_CONDITIONS(publicKey) + ); + + const conditions: ConditionType[] = conditionsScVal.map((cScVal) => { + assert( + cScVal.switch().name === xdr.ScValType.scvVec().name, + new E.INVALID_SCVAL_VEC_FOR_CONDITION(publicKey, cScVal.switch().name) + ); + return Condition.fromScVal(cScVal); + }); + return this.withdraw(publicKey, amount).addConditions(conditions); + } + + throw new E.UNSUPPORTED_OP_TYPE_FOR_SCVAL_CONVERSION(type); + } + //========================================== // Meta Requirement Methods //========================================== @@ -109,17 +276,28 @@ export class MoonlightOperation implements BaseOperation { private require(arg: "_publicKey"): Ed25519PublicKey; private require(arg: "_utxo"): UTXOPublicKey; private require(arg: "_conditions"): ConditionType[]; + private require(arg: "_utxoSignature"): OperationSignature; + private require(arg: "_ed25519Signature"): xdr.SorobanAuthorizationEntry; private require( - arg: "_op" | "_amount" | "_publicKey" | "_utxo" | "_conditions" + arg: + | "_op" + | "_amount" + | "_publicKey" + | "_utxo" + | "_conditions" + | "_utxoSignature" + | "_ed25519Signature" ): | UTXOOperationType | bigint | Ed25519PublicKey | UTXOPublicKey - | ConditionType[] { + | ConditionType[] + | OperationSignature + | xdr.SorobanAuthorizationEntry { if (this[arg] !== undefined) return this[arg]; - throw new Error(`Property ${arg} is not set in the Operation instance`); + throw new E.PROPERTY_NOT_SET(arg); } //========================================== @@ -218,6 +396,57 @@ export class MoonlightOperation implements BaseOperation { return this.require("_utxo"); } + /** + * Returns the UTXO signature for this operation. + * @returns The UTXO signature as an OperationSignature object + * @throws {Error} If the signature is not set + */ + public getUTXOSignature(): OperationSignature { + return this.require("_utxoSignature"); + } + + /** + * Sets the UTXO signature for this operation. + * @param signature - The OperationSignature to set + */ + private setUTXOSignature(signature: OperationSignature) { + this._utxoSignature = signature; + } + + public appendUTXOSignature(signature: OperationSignature): this { + assert(this.isSignedByUTXO() === false, new E.OP_ALREADY_SIGNED("UTXO")); + this.setUTXOSignature(signature); + return this; + } + + /** + * Returns the Ed25519 signature for this operation. + * @returns The Ed25519 signature as a SorobanAuthorizationEntry + * @throws {Error} If the signature is not set + */ + public getEd25519Signature(): xdr.SorobanAuthorizationEntry { + return this.require("_ed25519Signature"); + } + + /** + * Sets the Ed25519 signature for this operation. + * @param signature + */ + private setEd25519Signature(signature: xdr.SorobanAuthorizationEntry) { + this._ed25519Signature = signature; + } + + public appendEd25519Signature( + signature: xdr.SorobanAuthorizationEntry + ): this { + assert( + this.isSignedByEd25519() === false, + new E.OP_ALREADY_SIGNED("Ed25519") + ); + this.setEd25519Signature(signature); + return this; + } + //========================================== // Meta Management Methods //========================================== @@ -241,6 +470,106 @@ export class MoonlightOperation implements BaseOperation { return this; } + /** + * Signs the operation with a UTXO keypair. + * @param utxo The UTXO keypair to use for signing + * @param channelId The channel ID to include in the signature + * @param signatureExpirationLedger The ledger sequence number at which the signature expires + * @returns The signed operation + */ + public async signWithUTXO( + utxo: IUTXOKeypairBase, + channelId: ContractId, + signatureExpirationLedger: number + ): Promise { + assert(this.isSignedByUTXO() === false, new E.OP_ALREADY_SIGNED("UTXO")); + + assert( + this.getOperation() === UTXOOperationType.SPEND, + new E.OP_IS_NOT_SIGNABLE(this.getOperation(), "UTXO") + ); + + const conditions = this.getConditions(); + + assert(conditions.length > 0, new E.OP_HAS_NO_CONDITIONS(this.getUtxo())); + + const signedHash = await utxo.signPayload( + await buildAuthPayloadHash({ + contractId: channelId, + conditions, + liveUntilLedger: signatureExpirationLedger, + }) + ); + + this.appendUTXOSignature({ + sig: Buffer.from(signedHash), + exp: signatureExpirationLedger, + }); + + return this; + } + + public async signWithEd25519( + depositorKeys: TransactionSigner | Keypair, + signatureExpirationLedger: number, + channelId: ContractId, + assetId: ContractId, + networkPassphrase: string, + nonce?: string + ): Promise { + assert( + this.isSignedByEd25519() === false, + new E.OP_ALREADY_SIGNED("Ed25519") + ); + + assert( + this.getOperation() === UTXOOperationType.DEPOSIT, + new E.OP_IS_NOT_SIGNABLE(this.getOperation(), "Ed25519") + ); + + assert( + depositorKeys.publicKey() === this.getPublicKey(), + new E.SIGNER_IS_NOT_DEPOSITOR( + depositorKeys.publicKey(), + this.getPublicKey() + ) + ); + + if (!nonce) nonce = generateNonce(); + + const rawAuthEntry = await buildDepositAuthEntry({ + channelId, + assetId, + depositor: depositorKeys.publicKey() as Ed25519PublicKey, + amount: this.getAmount(), + conditions: [ + xdr.ScVal.scvVec(this.getConditions().map((c) => c.toScVal())), + ], + signatureExpirationLedger, + nonce, + }); + + let signedAuthEntry: xdr.SorobanAuthorizationEntry; + + if (isTransactionSigner(depositorKeys)) { + signedAuthEntry = await depositorKeys.signSorobanAuthEntry( + rawAuthEntry, + signatureExpirationLedger, + networkPassphrase + ); + } else { + signedAuthEntry = await authorizeEntry( + rawAuthEntry, + depositorKeys, + signatureExpirationLedger, + networkPassphrase + ); + } + + this.appendEd25519Signature(signedAuthEntry); + + return this; + } //========================================== // Type Guard Methods //========================================== @@ -329,6 +658,14 @@ export class MoonlightOperation implements BaseOperation { : false; } + public isSignedByUTXO(): boolean { + return this._utxoSignature !== undefined; + } + + public isSignedByEd25519(): boolean { + return this._ed25519Signature !== undefined; + } + //========================================== // Conversion Methods //========================================== @@ -346,7 +683,7 @@ export class MoonlightOperation implements BaseOperation { return Condition.withdraw(this.getPublicKey(), this.getAmount()); } - throw new Error("Cannot convert SPEND operation to Condition"); + throw new E.CANNOT_CONVERT_SPEND_OP(this.getUtxo()); } /** @@ -384,7 +721,7 @@ export class MoonlightOperation implements BaseOperation { return this.spendToScVal(); } - throw new Error("Unsupported operation type for ScVal conversion"); + throw new E.UNSUPPORTED_OP_TYPE_FOR_SCVAL_CONVERSION(this.getOperation()); } private conditionsToScVal(): xdr.ScVal { @@ -394,7 +731,7 @@ export class MoonlightOperation implements BaseOperation { } private createToScVal(): xdr.ScVal { - if (!this.isCreate()) throw new Error("Operation is not CREATE"); + assert(this.isCreate(), new E.OP_IS_NOT_CREATE(this.getOperation())); return xdr.ScVal.scvVec([ xdr.ScVal.scvBytes(Buffer.from(this.getUtxo())), @@ -403,7 +740,7 @@ export class MoonlightOperation implements BaseOperation { } private spendToScVal(): xdr.ScVal { - if (!this.isSpend()) throw new Error("Operation is not SPEND"); + assert(this.isSpend(), new E.OP_IS_NOT_SPEND(this.getOperation())); return xdr.ScVal.scvVec([ xdr.ScVal.scvBytes(Buffer.from(this.getUtxo())), this.conditionsToScVal(), @@ -411,7 +748,7 @@ export class MoonlightOperation implements BaseOperation { } private depositToScVal(): xdr.ScVal { - if (!this.isDeposit()) throw new Error("Operation is not DEPOSIT"); + assert(this.isDeposit(), new E.OP_IS_NOT_DEPOSIT(this.getOperation())); return xdr.ScVal.scvVec([ nativeToScVal(this.getPublicKey(), { type: "address" }), @@ -421,7 +758,7 @@ export class MoonlightOperation implements BaseOperation { } private withdrawToScVal(): xdr.ScVal { - if (!this.isWithdraw()) throw new Error("Operation is not WITHDRAW"); + assert(this.isWithdraw(), new E.OP_IS_NOT_WITHDRAW(this.getOperation())); return xdr.ScVal.scvVec([ nativeToScVal(this.getPublicKey(), { type: "address" }), @@ -447,4 +784,20 @@ export class MoonlightOperation implements BaseOperation { public toXDR(): string { return this.toScVal().toXDR("base64"); } + + public toMLXDR(): string { + return MLXDR.fromOperation( + this as + | CreateOperation + | DepositOperation + | WithdrawOperation + | SpendOperation + ); + } + + static fromMLXDR( + mlxdrString: string + ): CreateOperation | DepositOperation | WithdrawOperation | SpendOperation { + return MLXDR.toOperation(mlxdrString); + } } diff --git a/src/operation/index.unit.test.ts b/src/operation/index.unit.test.ts new file mode 100644 index 0000000..92f7fd2 --- /dev/null +++ b/src/operation/index.unit.test.ts @@ -0,0 +1,375 @@ +import { assertEquals, assertExists, assertThrows } from "@std/assert"; +import { beforeAll, describe, it } from "@std/testing/bdd"; +import { + type ContractId, + type Ed25519PublicKey, + LocalSigner, +} from "@colibri/core"; +import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; +import { generateP256KeyPair } from "../utils/secp256r1/generateP256KeyPair.ts"; +import { MoonlightOperation } from "./index.ts"; +import { UTXOOperationType } from "./types.ts"; +import type { CreateCondition } from "../conditions/types.ts"; +import { Condition } from "../conditions/index.ts"; +import { Asset, Networks } from "@stellar/stellar-sdk"; +import { UTXOKeypairBase } from "@moonlight/moonlight-sdk"; + +describe("Condition", () => { + let validPublicKey: Ed25519PublicKey; + let validUtxo: UTXOPublicKey; + + let channelId: ContractId; + + let assetId: ContractId; + let network: string; + + beforeAll(async () => { + validPublicKey = + LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + validUtxo = (await generateP256KeyPair()).publicKey as UTXOPublicKey; + + channelId = + "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC" as ContractId; + + network = Networks.TESTNET; + assetId = Asset.native().contractId(network) as ContractId; + }); + + describe(" ScValConversions", () => { + it("should convert to and from ScVal correctly for Create operation", () => { + const createOp = MoonlightOperation.create(validUtxo, 10n); + + const scVal = createOp.toScVal(); + const recreatedOp = MoonlightOperation.fromScVal( + scVal, + UTXOOperationType.CREATE + ); + + assertEquals(recreatedOp.getOperation(), UTXOOperationType.CREATE); + assertEquals(recreatedOp.getAmount(), 10n); + assertEquals(recreatedOp.getUtxo().toString(), validUtxo.toString()); + + assertThrows(() => { + MoonlightOperation.fromScVal(scVal, UTXOOperationType.DEPOSIT); + }); + + assertThrows(() => { + MoonlightOperation.fromScVal(scVal, UTXOOperationType.WITHDRAW); + }); + assertThrows(() => { + MoonlightOperation.fromScVal(scVal, UTXOOperationType.SPEND); + }); + }); + + it("should convert to and from ScVal correctly for Spend operation without conditions", () => { + const spendOp = MoonlightOperation.spend(validUtxo); + + const scVal = spendOp.toScVal(); + const recreatedOp = MoonlightOperation.fromScVal( + scVal, + UTXOOperationType.SPEND + ); + + assertEquals(recreatedOp.getOperation(), UTXOOperationType.SPEND); + assertEquals(recreatedOp.getUtxo().toString(), validUtxo.toString()); + assertEquals(recreatedOp.getConditions().length, 0); + + assertThrows(() => { + MoonlightOperation.fromScVal(scVal, UTXOOperationType.CREATE); + }); + + assertThrows(() => { + MoonlightOperation.fromScVal(scVal, UTXOOperationType.DEPOSIT); + }); + + assertThrows(() => { + MoonlightOperation.fromScVal(scVal, UTXOOperationType.WITHDRAW); + }); + }); + + it("should convert to and from ScVal correctly for Spend operation with conditions", () => { + const spendOp = MoonlightOperation.spend(validUtxo); + spendOp.addCondition( + MoonlightOperation.create(validUtxo, 500n).toCondition() + ); + spendOp.addCondition( + MoonlightOperation.create(validUtxo, 300n).toCondition() + ); + const scVal = spendOp.toScVal(); + const recreatedOp = MoonlightOperation.fromScVal( + scVal, + UTXOOperationType.SPEND + ); + + assertEquals(recreatedOp.getOperation(), UTXOOperationType.SPEND); + assertEquals(recreatedOp.getUtxo().toString(), validUtxo.toString()); + assertEquals(recreatedOp.getConditions().length, 2); + assertEquals(recreatedOp.getConditions()[0].isCreate(), true); + assertEquals( + (recreatedOp.getConditions()[0] as CreateCondition).getUtxo(), + validUtxo + ); + assertEquals(recreatedOp.getConditions()[0].getAmount(), 500n); + assertEquals(recreatedOp.getConditions()[1].isCreate(), true); + assertEquals( + (recreatedOp.getConditions()[1] as CreateCondition).getUtxo(), + validUtxo + ); + assertEquals(recreatedOp.getConditions()[1].getAmount(), 300n); + + assertThrows(() => { + MoonlightOperation.fromScVal(scVal, UTXOOperationType.CREATE); + }); + assertThrows(() => { + MoonlightOperation.fromScVal(scVal, UTXOOperationType.DEPOSIT); + }); + assertThrows(() => { + MoonlightOperation.fromScVal(scVal, UTXOOperationType.WITHDRAW); + }); + }); + + it("should convert to and from ScVal correctly for Deposit operation", () => { + const depositOp = MoonlightOperation.deposit(validPublicKey, 20n); + const scVal = depositOp.toScVal(); + const recreatedOp = MoonlightOperation.fromScVal( + scVal, + UTXOOperationType.DEPOSIT + ); + + assertEquals(recreatedOp.getOperation(), UTXOOperationType.DEPOSIT); + assertEquals(recreatedOp.getAmount(), 20n); + assertEquals( + recreatedOp.getPublicKey().toString(), + validPublicKey.toString() + ); + assertEquals(recreatedOp.getConditions().length, 0); + + assertThrows(() => { + MoonlightOperation.fromScVal(scVal, UTXOOperationType.CREATE); + }); + + assertThrows(() => { + MoonlightOperation.fromScVal(scVal, UTXOOperationType.SPEND); + }); + + // Cannot enforce against withdraw as it shares the same ScVal structure as Deposit + + depositOp.addCondition( + MoonlightOperation.create(validUtxo, 200n).toCondition() + ); + const scValWithCondition = depositOp.toScVal(); + const recreatedOpWithCondition = MoonlightOperation.fromScVal( + scValWithCondition, + UTXOOperationType.DEPOSIT + ); + + assertEquals( + recreatedOpWithCondition.getOperation(), + UTXOOperationType.DEPOSIT + ); + assertEquals(recreatedOpWithCondition.getAmount(), 20n); + assertEquals( + recreatedOpWithCondition.getPublicKey().toString(), + validPublicKey.toString() + ); + assertEquals(recreatedOpWithCondition.getConditions().length, 1); + assertEquals( + recreatedOpWithCondition.getConditions()[0].isCreate(), + true + ); + assertEquals( + ( + recreatedOpWithCondition.getConditions()[0] as CreateCondition + ).getUtxo(), + validUtxo + ); + assertEquals( + recreatedOpWithCondition.getConditions()[0].getAmount(), + 200n + ); + }); + + it("should convert to and from ScVal correctly for Withdraw operation ", () => { + const withdrawOp = MoonlightOperation.withdraw(validPublicKey, 30n); + const scVal = withdrawOp.toScVal(); + const recreatedOp = MoonlightOperation.fromScVal( + scVal, + UTXOOperationType.WITHDRAW + ); + + assertEquals(recreatedOp.getOperation(), UTXOOperationType.WITHDRAW); + assertEquals(recreatedOp.getAmount(), 30n); + assertEquals( + recreatedOp.getPublicKey().toString(), + validPublicKey.toString() + ); + assertEquals(recreatedOp.getConditions().length, 0); + + assertThrows(() => { + MoonlightOperation.fromScVal(scVal, UTXOOperationType.CREATE); + }); + + assertThrows(() => { + MoonlightOperation.fromScVal(scVal, UTXOOperationType.SPEND); + }); + + // Cannot enforce against deposit as it shares the same ScVal structure as Withdraw + + withdrawOp.addCondition( + MoonlightOperation.create(validUtxo, 300n).toCondition() + ); + const scValWithCondition = withdrawOp.toScVal(); + const recreatedOpWithCondition = MoonlightOperation.fromScVal( + scValWithCondition, + UTXOOperationType.WITHDRAW + ); + + assertEquals( + recreatedOpWithCondition.getOperation(), + UTXOOperationType.WITHDRAW + ); + assertEquals(recreatedOpWithCondition.getAmount(), 30n); + assertEquals( + recreatedOpWithCondition.getPublicKey().toString(), + validPublicKey.toString() + ); + assertEquals(recreatedOpWithCondition.getConditions().length, 1); + assertEquals( + recreatedOpWithCondition.getConditions()[0].isCreate(), + true + ); + assertEquals( + ( + recreatedOpWithCondition.getConditions()[0] as CreateCondition + ).getUtxo(), + validUtxo + ); + assertEquals( + recreatedOpWithCondition.getConditions()[0].getAmount(), + 300n + ); + }); + }); + + describe("MLXDRConversions", () => { + it("should convert to and from MLXDR correctly for Create operation", () => { + const createOp = MoonlightOperation.create(validUtxo, 10n); + + const mlxdr = createOp.toMLXDR(); + const recreatedOp = MoonlightOperation.fromMLXDR( + mlxdr + ) as MoonlightOperation; + assertEquals(recreatedOp.getOperation(), UTXOOperationType.CREATE); + assertEquals(recreatedOp.getAmount(), 10n); + assertEquals(recreatedOp.getUtxo().toString(), validUtxo.toString()); + }); + + it("should convert to and from MLXDR correctly for Deposit operation", () => { + const depositOp = MoonlightOperation.deposit(validPublicKey, 20n); + const mlxdr = depositOp.toMLXDR(); + const recreatedOp = MoonlightOperation.fromMLXDR( + mlxdr + ) as MoonlightOperation; + + assertEquals(recreatedOp.getOperation(), UTXOOperationType.DEPOSIT); + assertEquals(recreatedOp.getAmount(), 20n); + assertEquals( + recreatedOp.getPublicKey().toString(), + validPublicKey.toString() + ); + }); + + it("should convert to and from MLXDR correctly for Withdraw operation ", () => { + const withdrawOp = MoonlightOperation.withdraw(validPublicKey, 30n); + const mlxdr = withdrawOp.toMLXDR(); + const recreatedOp = MoonlightOperation.fromMLXDR( + mlxdr + ) as MoonlightOperation; + + assertEquals(recreatedOp.getOperation(), UTXOOperationType.WITHDRAW); + assertEquals(recreatedOp.getAmount(), 30n); + assertEquals( + recreatedOp.getPublicKey().toString(), + validPublicKey.toString() + ); + }); + + it("should convert to and from MLXDR correctly for Spend operation", () => { + const spendOp = MoonlightOperation.spend(validUtxo); + const mlxdr = spendOp.toMLXDR(); + const recreatedOp = MoonlightOperation.fromMLXDR( + mlxdr + ) as MoonlightOperation; + + assertEquals(recreatedOp.getOperation(), UTXOOperationType.SPEND); + assertEquals(recreatedOp.getUtxo().toString(), validUtxo.toString()); + + console.log("SPEND OP MLXDR:", mlxdr); + }); + + it("should convert to and from MLXDR correctly for signed operations", async () => { + const ed25519Signer = LocalSigner.generateRandom(); + const depositOp = MoonlightOperation.deposit( + ed25519Signer.publicKey() as Ed25519PublicKey, + 50n + ); + + depositOp.addCondition(Condition.create(validUtxo, 400n)); + + await depositOp.signWithEd25519( + ed25519Signer, + 100, + channelId, + assetId, + network + ); + + const mlxdr = depositOp.toMLXDR(); + + const recreatedOp = MoonlightOperation.fromMLXDR( + mlxdr + ) as MoonlightOperation; + + assertEquals(recreatedOp.getOperation(), UTXOOperationType.DEPOSIT); + assertEquals(recreatedOp.getAmount(), 50n); + assertEquals( + recreatedOp.getPublicKey().toString(), + ed25519Signer.publicKey().toString() + ); + assertEquals(recreatedOp.getConditions().length, 1); + assertExists(recreatedOp.getEd25519Signature()); + assertEquals( + recreatedOp.getEd25519Signature().toXDR(), + depositOp.getEd25519Signature().toXDR() + ); + + const utxoSigner = new UTXOKeypairBase(await generateP256KeyPair()); + const spendOp = MoonlightOperation.spend(utxoSigner.publicKey); + spendOp.addCondition(Condition.create(validUtxo, 600n)); + + await spendOp.signWithUTXO(utxoSigner, channelId, 100); + + const spendMlxdr = spendOp.toMLXDR(); + + const recreatedSpendOp = MoonlightOperation.fromMLXDR( + spendMlxdr + ) as MoonlightOperation; + + assertEquals(recreatedSpendOp.getOperation(), UTXOOperationType.SPEND); + assertEquals( + recreatedSpendOp.getUtxo().toString(), + utxoSigner.publicKey.toString() + ); + assertEquals(recreatedSpendOp.getConditions().length, 1); + assertExists(recreatedSpendOp.getUTXOSignature()); + assertEquals( + recreatedSpendOp.getUTXOSignature().sig, + spendOp.getUTXOSignature().sig + ); + assertEquals( + recreatedSpendOp.getUTXOSignature().exp, + spendOp.getUTXOSignature().exp + ); + }); + }); +}); diff --git a/src/operation/types.ts b/src/operation/types.ts index 8e7a92c..2bf3fa6 100644 --- a/src/operation/types.ts +++ b/src/operation/types.ts @@ -1,13 +1,21 @@ -import type { Ed25519PublicKey } from "@colibri/core"; +import type { + ContractId, + Ed25519PublicKey, + TransactionSigner, +} from "@colibri/core"; -import type { xdr } from "@stellar/stellar-sdk"; +import type { Keypair, xdr } from "@stellar/stellar-sdk"; import type { CreateCondition, DepositCondition, WithdrawCondition, } from "../conditions/types.ts"; import type { Condition } from "../conditions/types.ts"; -import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; +import type { + IUTXOKeypairBase, + UTXOPublicKey, +} from "../core/utxo-keypair-base/types.ts"; +import type { Buffer } from "buffer"; export enum UTXOOperationType { CREATE = "Create", @@ -30,6 +38,7 @@ export interface BaseOperation { clearConditions(): this; toXDR(): string; toScVal(): xdr.ScVal; + toMLXDR(): string; } export interface CreateOperation extends BaseOperation { @@ -42,6 +51,17 @@ export interface DepositOperation extends BaseOperation { getOperation(): UTXOOperationType.DEPOSIT; getPublicKey(): Ed25519PublicKey; toCondition(): DepositCondition; + signWithEd25519( + depositorKeys: TransactionSigner | Keypair, + signatureExpirationLedger: number, + channelId: ContractId, + assetId: ContractId, + networkPassphrase: string, + nonce?: string + ): Promise; + getEd25519Signature(): xdr.SorobanAuthorizationEntry; + isSignedByEd25519(): boolean; + appendEd25519Signature(signature: xdr.SorobanAuthorizationEntry): this; } export interface WithdrawOperation extends BaseOperation { @@ -53,6 +73,14 @@ export interface WithdrawOperation extends BaseOperation { export interface SpendOperation extends BaseOperation { getOperation(): UTXOOperationType.SPEND; getUtxo(): UTXOPublicKey; + isSignedByUTXO(): boolean; + getUTXOSignature(): OperationSignature; + signWithUTXO( + utxo: IUTXOKeypairBase, + channelId: ContractId, + signatureExpirationLedger: number + ): Promise; + appendUTXOSignature(signature: OperationSignature): this; } export type MoonlightOperation = @@ -60,3 +88,5 @@ export type MoonlightOperation = | DepositOperation | WithdrawOperation | SpendOperation; + +export type OperationSignature = { sig: Buffer; exp: number }; diff --git a/src/privacy-channel/error.ts b/src/privacy-channel/error.ts new file mode 100644 index 0000000..c540ac0 --- /dev/null +++ b/src/privacy-channel/error.ts @@ -0,0 +1,56 @@ +import { MoonlightError } from "../error/index.ts"; + +export type Meta = { + cause: Error | null; + data: unknown; +}; + +export type PrivacyChannelErrorShape = { + code: Code; + message: string; + details: string; + cause?: Error; + data: unknown; +}; + +export enum Code { + UNEXPECTED_ERROR = "PCH_000", + PROPERTY_NOT_SET = "PCH_001", +} + +export abstract class PrivacyChannelError extends MoonlightError { + override readonly meta: Meta; + + constructor(args: PrivacyChannelErrorShape) { + const meta = { + cause: args.cause || null, + data: args.data, + }; + + super({ + domain: "privacy-channel" as const, + source: "@Moonlight/privacy-channel", + code: args.code, + message: args.message, + details: args.details, + meta, + }); + + this.meta = meta; + } +} + +export class PROPERTY_NOT_SET extends PrivacyChannelError { + constructor(property: string) { + super({ + code: Code.PROPERTY_NOT_SET, + message: `Property not set: ${property}`, + details: `The required property ${property} is not set in the privacy channel.`, + data: { property }, + }); + } +} + +export const PCH_ERRORS = { + [Code.PROPERTY_NOT_SET]: PROPERTY_NOT_SET, +}; diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index 5475131..d10b8df 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -13,6 +13,7 @@ import { } from "./constants.ts"; import type { ChannelInvoke, ChannelRead } from "./types.ts"; import type { xdr } from "@stellar/stellar-sdk"; +import * as E from "./error.ts"; export class PrivacyChannel { private _client: Contract; @@ -66,7 +67,7 @@ export class PrivacyChannel { arg: "_client" | "_authId" | "_networkConfig" | "_derivator" | "_assetId" ): Contract | ContractId | NetworkConfig | StellarDerivator { if (this[arg]) return this[arg]; - throw new Error(`Property ${arg} is not set in the Channel instance.`); + throw new E.PROPERTY_NOT_SET(arg); } //========================================== diff --git a/src/transaction-builder/auth/index.ts b/src/transaction-builder/auth/index.ts index 58976bb..21f80d1 100644 --- a/src/transaction-builder/auth/index.ts +++ b/src/transaction-builder/auth/index.ts @@ -1,3 +1,3 @@ export * from "./bundle-auth-entry.ts"; -export * from "./deposit-auth-entry.ts"; + export * from "./payload-hash.ts"; diff --git a/src/transaction-builder/error.ts b/src/transaction-builder/error.ts new file mode 100644 index 0000000..519745b --- /dev/null +++ b/src/transaction-builder/error.ts @@ -0,0 +1,217 @@ +import type { Ed25519PublicKey } from "@colibri/core"; +import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; +import { MoonlightError } from "../error/index.ts"; + +export type Meta = { + cause: Error | null; + data: unknown; +}; + +export type TransactionBuilderErrorShape = { + code: Code; + message: string; + details: string; + cause?: Error; + data: unknown; +}; + + + +export enum Code { + UNEXPECTED_ERROR = "TBU_000", + PROPERTY_NOT_SET = "TBU_001", + UNSUPPORTED_OP_TYPE = "TBU_002", + DUPLICATE_CREATE_OP = "TBU_003", + DUPLICATE_SPEND_OP = "TBU_004", + DUPLICATE_DEPOSIT_OP = "TBU_005", + DUPLICATE_WITHDRAW_OP = "TBU_006", + AMOUNT_TOO_LOW = "TBU_007", + NO_SPEND_OPS = "TBU_008", + NO_DEPOSIT_OPS = "TBU_009", + NO_WITHDRAW_OPS = "TBU_010", + NO_EXT_OPS = "TBU_011", + MISSING_PROVIDER_SIGNATURE = "TBU_012", + NO_CONDITIONS_FOR_SPEND_OP = "TBU_013", +} + +export abstract class TransactionBuilderError extends MoonlightError< + Code, + Meta +> { + override readonly meta: Meta; + + constructor(args: TransactionBuilderErrorShape) { + const meta = { + cause: args.cause || null, + data: args.data, + }; + + super({ + domain: "transaction-builder" as const, + source: "@Moonlight/transaction-builder", + code: args.code, + message: args.message, + details: args.details, + meta, + }); + + this.meta = meta; + } +} + +export class PROPERTY_NOT_SET extends TransactionBuilderError { + constructor(property: string) { + super({ + code: Code.PROPERTY_NOT_SET, + message: `Property not set: ${property}`, + details: `The required property ${property} is not set in the transaction builder.`, + data: { property }, + }); + } +} + +export class UNSUPPORTED_OP_TYPE extends TransactionBuilderError { + constructor(opType: string) { + super({ + code: Code.UNSUPPORTED_OP_TYPE, + message: `Unsupported operation type: ${opType}`, + details: `The operation type ${opType} is not supported in the transaction builder.`, + data: { opType }, + }); + } +} + +export class DUPLICATE_CREATE_OP extends TransactionBuilderError { + constructor(utxoPk: UTXOPublicKey) { + super({ + code: Code.DUPLICATE_CREATE_OP, + message: `Duplicate create operation for UTXO public key: ${utxoPk}`, + details: `A create operation for the UTXO public key ${utxoPk} already exists in the transaction builder.`, + data: { utxoPk }, + }); + } +} + +export class DUPLICATE_SPEND_OP extends TransactionBuilderError { + constructor(utxoPk: UTXOPublicKey) { + super({ + code: Code.DUPLICATE_SPEND_OP, + message: `Duplicate spend operation for UTXO public key: ${utxoPk}`, + details: `A spend operation for the UTXO public key ${utxoPk} already exists in the transaction builder.`, + data: { utxoPk }, + }); + } +} + +export class DUPLICATE_DEPOSIT_OP extends TransactionBuilderError { + constructor(publicKey: Ed25519PublicKey) { + super({ + code: Code.DUPLICATE_DEPOSIT_OP, + message: `Duplicate deposit operation for public key: ${publicKey}`, + details: `A deposit operation for the public key ${publicKey} already exists in the transaction builder.`, + data: { publicKey }, + }); + } +} +export class DUPLICATE_WITHDRAW_OP extends TransactionBuilderError { + constructor(publicKey: Ed25519PublicKey) { + super({ + code: Code.DUPLICATE_WITHDRAW_OP, + message: `Duplicate withdraw operation for public key: ${publicKey}`, + details: `A withdraw operation for the public key ${publicKey} already exists in the transaction builder.`, + data: { publicKey }, + }); + } +} + +export class NO_SPEND_OPS extends TransactionBuilderError { + constructor(utxoPk: UTXOPublicKey) { + super({ + code: Code.NO_SPEND_OPS, + message: `No spend operations found for the UTXO: ${utxoPk}`, + details: `There are no spend operations associated with the UTXO public key ${utxoPk} in the transaction builder.`, + data: { utxoPk }, + }); + } +} + +export class NO_DEPOSIT_OPS extends TransactionBuilderError { + constructor(publicKey: Ed25519PublicKey) { + super({ + code: Code.NO_DEPOSIT_OPS, + message: `No deposit operations found for the public key: ${publicKey}`, + details: `There are no deposit operations associated with the public key ${publicKey} in the transaction builder.`, + data: { publicKey }, + }); + } +} + +export class NO_WITHDRAW_OPS extends TransactionBuilderError { + constructor(publicKey: Ed25519PublicKey) { + super({ + code: Code.NO_WITHDRAW_OPS, + message: `No withdraw operations found for the public key: ${publicKey}`, + details: `There are no withdraw operations associated with the public key ${publicKey} in the transaction builder.`, + data: { publicKey }, + }); + } +} + +export class NO_EXT_OPS extends TransactionBuilderError { + constructor(publicKey: Ed25519PublicKey) { + super({ + code: Code.NO_EXT_OPS, + message: `No deposit or withdraw operations found for the public key: ${publicKey}`, + details: `There are no deposit or withdraw operations associated with the public key ${publicKey} in the transaction builder.`, + data: { publicKey }, + }); + } +} + +export class AMOUNT_TOO_LOW extends TransactionBuilderError { + constructor(amount: bigint) { + super({ + code: Code.AMOUNT_TOO_LOW, + message: `Amount too low: ${amount}`, + details: `The provided amount ${amount} is below the minimum required. It must be greater than zero.`, + data: { amount: `${amount}` }, + }); + } +} + +export class MISSING_PROVIDER_SIGNATURE extends TransactionBuilderError { + constructor() { + super({ + code: Code.MISSING_PROVIDER_SIGNATURE, + message: `Missing provider signature`, + details: `No provider signatures have been added to the transaction builder.`, + data: {}, + }); + } +} + +export class NO_CONDITIONS_FOR_SPEND_OP extends TransactionBuilderError { + constructor(utxoPk: UTXOPublicKey) { + super({ + code: Code.NO_CONDITIONS_FOR_SPEND_OP, + message: `No conditions found for spend operation with UTXO: ${utxoPk}`, + details: `The spend operation associated with the UTXO public key ${utxoPk} does not have any conditions set in the transaction builder.`, + data: { utxoPk }, + }); + } +} + +export const TBU_ERRORS = { + [Code.PROPERTY_NOT_SET]: PROPERTY_NOT_SET, + [Code.UNSUPPORTED_OP_TYPE]: UNSUPPORTED_OP_TYPE, + [Code.DUPLICATE_CREATE_OP]: DUPLICATE_CREATE_OP, + [Code.DUPLICATE_SPEND_OP]: DUPLICATE_SPEND_OP, + [Code.DUPLICATE_DEPOSIT_OP]: DUPLICATE_DEPOSIT_OP, + [Code.DUPLICATE_WITHDRAW_OP]: DUPLICATE_WITHDRAW_OP, + [Code.NO_SPEND_OPS]: NO_SPEND_OPS, + [Code.AMOUNT_TOO_LOW]: AMOUNT_TOO_LOW, + [Code.NO_DEPOSIT_OPS]: NO_DEPOSIT_OPS, + [Code.NO_WITHDRAW_OPS]: NO_WITHDRAW_OPS, + [Code.NO_EXT_OPS]: NO_EXT_OPS, + [Code.MISSING_PROVIDER_SIGNATURE]: MISSING_PROVIDER_SIGNATURE, +}; diff --git a/src/transaction-builder/index.ts b/src/transaction-builder/index.ts index 30b6fbd..48d090a 100644 --- a/src/transaction-builder/index.ts +++ b/src/transaction-builder/index.ts @@ -1,15 +1,8 @@ -import { - type Asset, - authorizeEntry, - type Keypair, - Operation, - xdr, -} from "@stellar/stellar-sdk"; +import { type Keypair, Operation, xdr } from "@stellar/stellar-sdk"; import { Buffer } from "buffer"; import { generateNonce } from "../utils/common/index.ts"; -import { buildAuthPayloadHash } from "../utils/auth/build-auth-payload.ts"; import type { IUTXOKeypairBase, UTXOPublicKey, @@ -18,15 +11,14 @@ import type { import { buildSignaturesXDR } from "./signatures/index.ts"; import { buildBundleAuthEntry, - buildDepositAuthEntry, buildOperationAuthEntryHash, } from "./auth/index.ts"; import { orderSpendByUtxo } from "./utils/index.ts"; import { - assertPositiveAmount, assertNoDuplicateCreate, assertNoDuplicateSpend, - assertNoDuplicatePubKey, + assertNoDuplicateDeposit, + assertNoDuplicateWithdraw, assertSpendExists, } from "./validators/index.ts"; import { @@ -41,7 +33,12 @@ import type { SpendOperation, WithdrawOperation, MoonlightOperation, + BaseOperation, + OperationSignature, } from "../operation/types.ts"; +import * as E from "./error.ts"; +import { assert } from "../utils/assert/assert.ts"; +import { assertExtOpsExist } from "./validators/operations.ts"; export class MoonlightTransactionBuilder { private _create: CreateOperation[] = []; @@ -50,7 +47,7 @@ export class MoonlightTransactionBuilder { private _withdraw: WithdrawOperation[] = []; private _channelId: ContractId; private _authId: ContractId; - private _asset: Asset; + private _assetId: ContractId; private _network: string; private _innerSignatures: Map = new Map(); @@ -64,17 +61,17 @@ export class MoonlightTransactionBuilder { constructor({ channelId, authId, - asset, + assetId, network, }: { channelId: ContractId; authId: ContractId; - asset: Asset; + assetId: ContractId; network: string; }) { this._channelId = channelId; this._authId = authId; - this._asset = asset; + this._assetId = assetId; this._network = network; } @@ -97,7 +94,7 @@ export class MoonlightTransactionBuilder { private require(arg: "_withdraw"): WithdrawOperation[]; private require(arg: "_channelId"): ContractId; private require(arg: "_authId"): ContractId; - private require(arg: "_asset"): Asset; + private require(arg: "_assetId"): ContractId; private require(arg: "_network"): string; private require( arg: "_innerSignatures" @@ -116,7 +113,7 @@ export class MoonlightTransactionBuilder { | "_withdraw" | "_channelId" | "_authId" - | "_asset" + | "_assetId" | "_network" | "_innerSignatures" | "_providerInnerSignatures" @@ -127,15 +124,12 @@ export class MoonlightTransactionBuilder { | DepositOperation[] | WithdrawOperation[] | ContractId - | Asset | string | Map | Map | Map { if (this[arg] !== undefined) return this[arg]; - throw new Error( - `Property ${arg} is not set in the Transaction Builder instance` - ); + throw new E.PROPERTY_NOT_SET(arg); } //========================================== @@ -243,13 +237,13 @@ export class MoonlightTransactionBuilder { } /** - * Returns the asset associated with the transaction. + * Returns the contract id of the asset associated with the transaction. * * @returns The asset * @throws {Error} If the asset is not set */ - public getAsset(): Asset { - return this.require("_asset"); + public getAssetId(): ContractId { + return this.require("_assetId"); } /** @@ -307,12 +301,13 @@ export class MoonlightTransactionBuilder { if (op.isSpend()) return this.addSpend(op); if (op.isDeposit()) return this.addDeposit(op); if (op.isWithdraw()) return this.addWithdraw(op); - throw new Error("Unsupported operation type"); + + throw new E.UNSUPPORTED_OP_TYPE((op as BaseOperation).getOperation()); } private addCreate(op: CreateOperation): MoonlightTransactionBuilder { assertNoDuplicateCreate(this.getCreateOperations(), op); - assertPositiveAmount(op.getAmount(), "Create operation"); + assert(op.getAmount() > 0n, new E.AMOUNT_TOO_LOW(op.getAmount())); this.setCreateOperations([...this.getCreateOperations(), op]); return this; @@ -322,36 +317,41 @@ export class MoonlightTransactionBuilder { assertNoDuplicateSpend(this.getSpendOperations(), op); this.setSpendOperations([...this.getSpendOperations(), op]); + + if (op.isSignedByUTXO()) { + this.addInnerSignature(op.getUtxo(), op.getUTXOSignature()); + } + return this; } private addDeposit(op: DepositOperation): MoonlightTransactionBuilder { - assertNoDuplicatePubKey(this.getDepositOperations(), op, "Deposit"); - assertPositiveAmount(op.getAmount(), "Deposit operation"); + assertNoDuplicateDeposit(this.getDepositOperations(), op); + assert(op.getAmount() > 0n, new E.AMOUNT_TOO_LOW(op.getAmount())); this.setDepositOperations([...this.getDepositOperations(), op]); + + if (op.isSignedByEd25519()) { + this.addExtSignedEntry(op.getPublicKey(), op.getEd25519Signature()); + } return this; } private addWithdraw(op: WithdrawOperation): MoonlightTransactionBuilder { - assertNoDuplicatePubKey(this.getWithdrawOperations(), op, "Withdraw"); - assertPositiveAmount(op.getAmount(), "Withdraw operation"); + assertNoDuplicateWithdraw(this.getWithdrawOperations(), op); + assert(op.getAmount() > 0n, new E.AMOUNT_TOO_LOW(op.getAmount())); this.setWithdrawOperations([...this.getWithdrawOperations(), op]); return this; } - public addInnerSignature( + private addInnerSignature( utxo: UTXOPublicKey, - signature: Buffer, - expirationLedger: number + signature: OperationSignature ): MoonlightTransactionBuilder { assertSpendExists(this.getSpendOperations(), utxo); - this.innerSignatures.set(utxo, { - sig: signature, - exp: expirationLedger, - }); + this.innerSignatures.set(utxo, signature); return this; } @@ -373,11 +373,11 @@ export class MoonlightTransactionBuilder { pubKey: Ed25519PublicKey, signedAuthEntry: xdr.SorobanAuthorizationEntry ): MoonlightTransactionBuilder { - if ( - !this.getDepositOperations().find((d) => d.getPublicKey() === pubKey) && - !this.getWithdrawOperations().find((d) => d.getPublicKey() === pubKey) - ) - throw new Error("No deposit or withdraw operation for this public key"); + assertExtOpsExist( + this.getDepositOperations(), + this.getWithdrawOperations(), + pubKey + ); this.extSignatures.set(pubKey, signedAuthEntry); return this; @@ -391,27 +391,34 @@ export class MoonlightTransactionBuilder { ); } - public getExtAuthEntry( - address: Ed25519PublicKey, - nonce: string, - signatureExpirationLedger: number - ): xdr.SorobanAuthorizationEntry { - const deposit = this.getDepositOperation(address); - if (!deposit) throw new Error("No deposit operation for this address"); - - return buildDepositAuthEntry({ - channelId: this.getChannelId(), - assetId: this.getAsset().contractId(this.network), - depositor: address, - amount: deposit.getAmount(), - conditions: [ - xdr.ScVal.scvVec(deposit.getConditions().map((c) => c.toScVal())), - ], - nonce, - signatureExpirationLedger, - }); + public getSpendOperation(utxo: UTXOPublicKey): SpendOperation | undefined { + return this.getSpendOperations().find((s) => + Buffer.from(s.getUtxo()).equals(Buffer.from(utxo)) + ); } + // public getExtAuthEntry( + // address: Ed25519PublicKey, + // nonce: string, + // signatureExpirationLedger: number + // ): xdr.SorobanAuthorizationEntry { + // const deposit = this.getDepositOperation(address); + + // assert(deposit, new E.NO_DEPOSIT_OPS(address)); + + // return buildDepositAuthEntry({ + // channelId: this.getChannelId(), + // assetId: this.getAsset().contractId(this.network), + // depositor: address, + // amount: deposit.getAmount(), + // conditions: [ + // xdr.ScVal.scvVec(deposit.getConditions().map((c) => c.toScVal())), + // ], + // nonce, + // signatureExpirationLedger, + // }); + // } + public getAuthRequirementArgs(): xdr.ScVal[] { if (this.getSpendOperations().length === 0) return []; @@ -490,8 +497,8 @@ export class MoonlightTransactionBuilder { public signaturesXDR(): string { const providerSigners = Array.from(this.providerInnerSignatures.keys()); - if (providerSigners.length === 0) - throw new Error("No Provider signatures added"); + + assert(providerSigners.length > 0, new E.MISSING_PROVIDER_SIGNATURE()); const spendSigs = Array.from(this.innerSignatures.entries()).map( ([utxo, { sig, exp }]) => ({ utxo, sig, exp }) @@ -530,28 +537,20 @@ export class MoonlightTransactionBuilder { } public async signWithSpendUtxo( - utxo: IUTXOKeypairBase, + utxoKp: IUTXOKeypairBase, signatureExpirationLedger: number ) { - const conditions = this.getSpendOperations() - .find((s) => Buffer.from(s.getUtxo()).equals(Buffer.from(utxo.publicKey))) - ?.getConditions(); - - if (!conditions) throw new Error("No spend operation for this UTXO"); - - const signedHash = await utxo.signPayload( - await buildAuthPayloadHash({ - contractId: this.getChannelId(), - conditions, - liveUntilLedger: signatureExpirationLedger, - }) - ); + const spendOp = this.getSpendOperation(utxoKp.publicKey); + + assert(spendOp, new E.NO_SPEND_OPS(utxoKp.publicKey)); - this.addInnerSignature( - utxo.publicKey, - Buffer.from(signedHash), + await spendOp.signWithUTXO( + utxoKp, + this.getChannelId(), signatureExpirationLedger ); + + this.addInnerSignature(utxoKp.publicKey, spendOp.getUTXOSignature()); } public async signExtWithEd25519( @@ -559,33 +558,27 @@ export class MoonlightTransactionBuilder { signatureExpirationLedger: number, nonce?: string ) { - if (!nonce) nonce = generateNonce(); + const depositOp = this.getDepositOperation( + keys.publicKey() as Ed25519PublicKey + ); - const rawAuthEntry = this.getExtAuthEntry( - keys.publicKey() as Ed25519PublicKey, - nonce, - signatureExpirationLedger + assert( + depositOp, + new E.NO_DEPOSIT_OPS(keys.publicKey() as Ed25519PublicKey) ); - let signedAuthEntry: xdr.SorobanAuthorizationEntry; - if (isTransactionSigner(keys)) { - signedAuthEntry = await keys.signSorobanAuthEntry( - rawAuthEntry, - signatureExpirationLedger, - this.network - ); - } else { - signedAuthEntry = await authorizeEntry( - rawAuthEntry, - keys, - signatureExpirationLedger, - this.network - ); - } + await depositOp.signWithEd25519( + keys, + signatureExpirationLedger, + this.getChannelId(), + this.getAssetId(), + this.network, + nonce + ); this.addExtSignedEntry( keys.publicKey() as Ed25519PublicKey, - signedAuthEntry + depositOp.getEd25519Signature() ); } diff --git a/src/transaction-builder/index.unit.test.ts b/src/transaction-builder/index.unit.test.ts index e0f9008..b109899 100644 --- a/src/transaction-builder/index.unit.test.ts +++ b/src/transaction-builder/index.unit.test.ts @@ -1,4 +1,9 @@ -import { assertEquals, assertExists, assertThrows } from "@std/assert"; +import { + assertEquals, + assertExists, + assertRejects, + assertThrows, +} from "@std/assert"; import { beforeAll, describe, it } from "@std/testing/bdd"; import { LocalSigner } from "@colibri/core"; import { Asset, Networks } from "@stellar/stellar-sdk"; @@ -12,6 +17,8 @@ import { Buffer } from "buffer"; import { generateNonce } from "../utils/common/index.ts"; import { UTXOKeypairBase } from "../core/utxo-keypair-base/index.ts"; import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; +import * as OPR_ERR from "../operation/error.ts"; +import * as TBU_ERR from "./error.ts"; describe("MoonlightTransactionBuilder", () => { let validPublicKey: Ed25519PublicKey; @@ -19,7 +26,7 @@ describe("MoonlightTransactionBuilder", () => { let validAmount: bigint; let channelId: ContractId; let authId: ContractId; - let asset: Asset; + let assetId: ContractId; let network: string; let builder: MoonlightTransactionBuilder; @@ -32,13 +39,13 @@ describe("MoonlightTransactionBuilder", () => { "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC" as ContractId; authId = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA" as ContractId; - asset = Asset.native(); network = Networks.TESTNET; + assetId = Asset.native().contractId(network) as ContractId; builder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); }); @@ -48,21 +55,21 @@ describe("MoonlightTransactionBuilder", () => { const txBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); assertExists(txBuilder); assertEquals(txBuilder.getChannelId(), channelId); assertEquals(txBuilder.getAuthId(), authId); - assertEquals(txBuilder.getAsset(), asset); + assertEquals(txBuilder.getAssetId(), assetId); }); it("should initialize with empty operation arrays", () => { const txBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -83,7 +90,7 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -104,7 +111,7 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -126,8 +133,9 @@ describe("MoonlightTransactionBuilder", () => { }); describe("Signature Management", () => { - it("should add inner signatures for UTXO operations", async () => { - const utxo = (await generateP256KeyPair()).publicKey as UTXOPublicKey; + it("should add signed UTXO operations", async () => { + const utxoKp = new UTXOKeypairBase(await generateP256KeyPair()); + const utxo = utxoKp.publicKey as UTXOPublicKey; const condition = Condition.create(utxo, validAmount); const spendOperation = Operation.spend(utxo); spendOperation.addCondition(condition); @@ -135,19 +143,17 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); + const expirationLedger = 1000000; + await spendOperation.signWithUTXO(utxoKp, channelId, expirationLedger); + // Add spend operation first // deno-lint-ignore no-explicit-any (testBuilder as any).addSpend(spendOperation); - const signature = Buffer.from("test_signature"); - const expirationLedger = 1000000; - - testBuilder.addInnerSignature(utxo, signature, expirationLedger); - // deno-lint-ignore no-explicit-any const signatures = (testBuilder as any).innerSignatures; assertEquals(signatures.size, 1); @@ -177,7 +183,7 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -203,7 +209,7 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -228,10 +234,16 @@ describe("MoonlightTransactionBuilder", () => { const utxo = utxoKeys.publicKey as UTXOPublicKey; const spendOperation = Operation.spend(utxo); + spendOperation.addCondition( + Condition.create( + (await generateP256KeyPair()).publicKey as UTXOPublicKey, + 1n + ) + ); const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -260,7 +272,7 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -283,9 +295,9 @@ describe("MoonlightTransactionBuilder", () => { assertEquals(extSigs.has(pubKey), true); }); - it("should add external signed entry", () => { - const pubKey = - LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + it("should add external signed entry", async () => { + const depositorKeys = LocalSigner.generateRandom(); + const pubKey = depositorKeys.publicKey() as Ed25519PublicKey; const condition = Condition.deposit(pubKey, validAmount); const operation = Operation.deposit(pubKey, validAmount); operation.addCondition(condition); @@ -293,22 +305,23 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); - // deno-lint-ignore no-explicit-any - (testBuilder as any).addDeposit(operation); - const nonce = generateNonce(); const signatureExpirationLedger = 1000000; - const authEntry = testBuilder.getExtAuthEntry( - pubKey, - nonce, - signatureExpirationLedger - ); - testBuilder.addExtSignedEntry(pubKey, authEntry); + await operation.signWithEd25519( + depositorKeys, + signatureExpirationLedger, + channelId, + assetId, + network, + nonce + ); + // deno-lint-ignore no-explicit-any + (testBuilder as any).addDeposit(operation); // deno-lint-ignore no-explicit-any const extSigs = (testBuilder as any).extSignatures; @@ -338,7 +351,7 @@ describe("MoonlightTransactionBuilder", () => { const builderWithOps = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -378,37 +391,6 @@ describe("MoonlightTransactionBuilder", () => { assertEquals(hash.length > 0, true); }); - it("should generate external auth entry for deposit operation", () => { - const pubKey = - LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; - const condition = Condition.deposit(pubKey, validAmount); - const operation = Operation.deposit(pubKey, validAmount); - operation.addCondition(condition); - - const testBuilder = new MoonlightTransactionBuilder({ - channelId, - authId, - asset, - network, - }); - - // Add deposit operation - // deno-lint-ignore no-explicit-any - (testBuilder as any).addDeposit(operation); - - const nonce = generateNonce(); - const signatureExpirationLedger = 1000000; - - const authEntry = testBuilder.getExtAuthEntry( - pubKey, - nonce, - signatureExpirationLedger - ); - - assertExists(authEntry); - assertEquals(typeof authEntry.toXDR, "function"); - }); - it("should get auth requirement args for operation with spend operations", async () => { const utxoKeys = await generateP256KeyPair(); const utxo = utxoKeys.publicKey as UTXOPublicKey; @@ -419,7 +401,7 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -443,7 +425,7 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -461,7 +443,7 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -487,7 +469,7 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -528,7 +510,7 @@ describe("MoonlightTransactionBuilder", () => { const builderWithSigs = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -559,7 +541,7 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -594,7 +576,7 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -626,7 +608,7 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -634,8 +616,7 @@ describe("MoonlightTransactionBuilder", () => { assertThrows( () => testBuilder.addOperation(operation2), - Error, - "Create operation for this UTXO already exists" + TBU_ERR.DUPLICATE_CREATE_OP ); }); @@ -651,7 +632,7 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -661,8 +642,7 @@ describe("MoonlightTransactionBuilder", () => { assertThrows( // deno-lint-ignore no-explicit-any () => (testBuilder as any).addDeposit(operation2), - Error, - "Deposit operation for this public key already exists" + TBU_ERR.DUPLICATE_DEPOSIT_OP ); }); @@ -678,7 +658,7 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -688,18 +668,16 @@ describe("MoonlightTransactionBuilder", () => { assertThrows( // deno-lint-ignore no-explicit-any () => (testBuilder as any).addWithdraw(operation2), - Error, - "Withdraw operation for this public key already exists" + TBU_ERR.DUPLICATE_WITHDRAW_OP ); }); it("should throw error for zero amount in CREATE operation", async () => { const utxo = (await generateP256KeyPair()).publicKey as UTXOPublicKey; - assertThrows( - () => Operation.create(utxo, 0n), - Error, - "Amount must be greater than zero" + assertRejects( + async () => await Operation.create(utxo, 0n), + OPR_ERR.AMOUNT_TOO_LOW ); }); @@ -709,8 +687,7 @@ describe("MoonlightTransactionBuilder", () => { assertThrows( () => Operation.deposit(pubKey, -100n), - Error, - "Amount must be greater than zero" + OPR_ERR.AMOUNT_TOO_LOW ); }); @@ -719,36 +696,30 @@ describe("MoonlightTransactionBuilder", () => { LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; assertThrows( - () => Operation.withdraw(pubKey, -100n), - Error, - "Amount must be greater than zero" + () => Operation.withdraw(pubKey, -10n), + OPR_ERR.AMOUNT_TOO_LOW ); }); }); describe("Signature Validation", () => { it("should throw error when adding inner signature without spend operation", async () => { - const signature = Buffer.from("test_signature"); const expirationLedger = 1000000; - const nonExistentUtxo = (await generateP256KeyPair()) - .publicKey as UTXOPublicKey; + const nonExistentUtxo = new UTXOKeypairBase( + await generateP256KeyPair() + ); const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); - assertThrows( + await assertRejects( () => - testBuilder.addInnerSignature( - nonExistentUtxo, - signature, - expirationLedger - ), - Error, - "No spend operation for this UTXO" + testBuilder.signWithSpendUtxo(nonExistentUtxo, expirationLedger), + TBU_ERR.NO_SPEND_OPS ); }); @@ -761,14 +732,13 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); assertThrows( () => testBuilder.addExtSignedEntry(nonExistentKey, mockAuthEntry), - Error, - "No deposit or withdraw operation for this public key" + TBU_ERR.NO_EXT_OPS ); }); @@ -776,14 +746,13 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); assertThrows( () => testBuilder.signaturesXDR(), - Error, - "No Provider signatures added" + TBU_ERR.MISSING_PROVIDER_SIGNATURE ); }); @@ -791,7 +760,7 @@ describe("MoonlightTransactionBuilder", () => { const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); @@ -804,28 +773,26 @@ describe("MoonlightTransactionBuilder", () => { }); describe("External Auth Entry Validation", () => { - it("should throw error when getting external auth entry for non-existent deposit", () => { - const nonExistentKey = - LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + it("should throw error when getting external auth entry for non-existent deposit", async () => { + const nonExistentKey = LocalSigner.generateRandom(); const nonce = generateNonce(); const signatureExpirationLedger = 1000000; const testBuilder = new MoonlightTransactionBuilder({ channelId, authId, - asset, + assetId, network, }); - assertThrows( - () => - testBuilder.getExtAuthEntry( + await assertRejects( + async () => + await testBuilder.signExtWithEd25519( nonExistentKey, - nonce, - signatureExpirationLedger + signatureExpirationLedger, + nonce ), - Error, - "No deposit operation for this address" + TBU_ERR.NO_DEPOSIT_OPS ); }); }); @@ -839,8 +806,7 @@ describe("MoonlightTransactionBuilder", () => { assertThrows( // deno-lint-ignore no-explicit-any () => (emptyBuilder as any).require("_channelId"), - Error, - "Property _channelId is not set in the Transaction Builder instance" + TBU_ERR.PROPERTY_NOT_SET ); }); }); diff --git a/src/transaction-builder/validators/operations.ts b/src/transaction-builder/validators/operations.ts index 5f7c0c5..e9e0df4 100644 --- a/src/transaction-builder/validators/operations.ts +++ b/src/transaction-builder/validators/operations.ts @@ -1,10 +1,7 @@ import { Buffer } from "buffer"; import type { UTXOPublicKey } from "../../core/utxo-keypair-base/types.ts"; import type { Ed25519PublicKey } from "@colibri/core"; - -export const assertPositiveAmount = (amount: bigint, context: string) => { - if (amount <= 0n) throw new Error(`${context} amount must be positive`); -}; +import * as E from "../error.ts"; export const assertNoDuplicateCreate = ( existing: { getUtxo(): UTXOPublicKey }[], @@ -15,7 +12,7 @@ export const assertNoDuplicateCreate = ( Buffer.from(c.getUtxo()).equals(Buffer.from(op.getUtxo())) ) ) - throw new Error("Create operation for this UTXO already exists"); + throw new E.DUPLICATE_CREATE_OP(op.getUtxo()); }; export const assertNoDuplicateSpend = ( @@ -27,16 +24,23 @@ export const assertNoDuplicateSpend = ( Buffer.from(s.getUtxo()).equals(Buffer.from(op.getUtxo())) ) ) - throw new Error("Spend operation for this UTXO already exists"); + throw new E.DUPLICATE_SPEND_OP(op.getUtxo()); +}; + +export const assertNoDuplicateDeposit = ( + existing: { getPublicKey(): Ed25519PublicKey }[], + op: { getPublicKey(): Ed25519PublicKey } +) => { + if (existing.find((d) => d.getPublicKey() === op.getPublicKey())) + throw new E.DUPLICATE_DEPOSIT_OP(op.getPublicKey()); }; -export const assertNoDuplicatePubKey = ( +export const assertNoDuplicateWithdraw = ( existing: { getPublicKey(): Ed25519PublicKey }[], - op: { getPublicKey(): Ed25519PublicKey }, - context: string + op: { getPublicKey(): Ed25519PublicKey } ) => { if (existing.find((d) => d.getPublicKey() === op.getPublicKey())) - throw new Error(`${context} operation for this public key already exists`); + throw new E.DUPLICATE_WITHDRAW_OP(op.getPublicKey()); }; export const assertSpendExists = ( @@ -44,5 +48,33 @@ export const assertSpendExists = ( utxo: UTXOPublicKey ) => { if (!existing.find((s) => Buffer.from(s.getUtxo()).equals(Buffer.from(utxo)))) - throw new Error("No spend operation for this UTXO"); + throw new E.NO_SPEND_OPS(utxo); +}; + +export const assertExtOpsExist = ( + existingDeposit: { getPublicKey(): Ed25519PublicKey }[], + existingWithdraw: { getPublicKey(): Ed25519PublicKey }[], + pubKey: Ed25519PublicKey +) => { + if ( + !existingDeposit.find((d) => d.getPublicKey() === pubKey) && + !existingWithdraw.find((w) => w.getPublicKey() === pubKey) + ) + throw new E.NO_EXT_OPS(pubKey); +}; + +export const assertDepositExists = ( + existing: { getPublicKey(): Ed25519PublicKey }[], + pubKey: Ed25519PublicKey +) => { + if (!existing.find((d) => d.getPublicKey() === pubKey)) + throw new E.NO_DEPOSIT_OPS(pubKey); +}; + +export const assertWithdrawExists = ( + existing: { getPublicKey(): Ed25519PublicKey }[], + pubKey: Ed25519PublicKey +) => { + if (!existing.find((d) => d.getPublicKey() === pubKey)) + throw new E.NO_WITHDRAW_OPS(pubKey); }; diff --git a/src/utils/assert/assert-args.ts b/src/utils/assert/assert-args.ts new file mode 100644 index 0000000..0437c1c --- /dev/null +++ b/src/utils/assert/assert-args.ts @@ -0,0 +1,13 @@ +import type { MoonlightError } from "../../error/index.ts"; + +// Asserts that all provided arguments are neither null nor undefined. +// Throws the provided error if any argument is invalid. +export function assertRequiredArgs( + args: Record, + errorFn: (argName: string) => MoonlightError +): asserts args is Record { + for (const argName of Object.keys(args)) { + if (!(argName in args) || args[argName] === undefined) + throw errorFn(argName); + } +} diff --git a/src/utils/assert/assert.ts b/src/utils/assert/assert.ts new file mode 100644 index 0000000..06334f3 --- /dev/null +++ b/src/utils/assert/assert.ts @@ -0,0 +1,13 @@ +/** + * Asserts that the given condition is true. + * Throws the provided error if the condition is false. + * + * @param condition - The condition to check + * @param error - The error to throw if condition is false + * @throws The provided error if condition is false + */ +export function assert(condition: unknown, error: Error): asserts condition { + if (!condition) { + throw error; + } +} diff --git a/src/utils/assert/index.ts b/src/utils/assert/index.ts new file mode 100644 index 0000000..7d48c49 --- /dev/null +++ b/src/utils/assert/index.ts @@ -0,0 +1,2 @@ +export * from "./assert-args.ts"; +export * from "./assert.ts"; diff --git a/src/transaction-builder/auth/deposit-auth-entry.ts b/src/utils/auth/deposit-auth-entry copy.ts similarity index 100% rename from src/transaction-builder/auth/deposit-auth-entry.ts rename to src/utils/auth/deposit-auth-entry copy.ts diff --git a/src/utxo-based-account/error.ts b/src/utxo-based-account/error.ts new file mode 100644 index 0000000..5450221 --- /dev/null +++ b/src/utxo-based-account/error.ts @@ -0,0 +1,96 @@ +import { MoonlightError } from "../error/index.ts"; + +export type Meta = { + cause: Error | null; + data: unknown; +}; + +export type UTXOBasedAccountErrorShape = { + code: Code; + message: string; + details: string; + cause?: Error; + data: unknown; +}; + +export enum Code { + UNEXPECTED_ERROR = "UBA_000", + NEGATIVE_INDEX = "UBA_001", + UTXO_TO_DERIVE_TOO_LOW = "UBA_002", + MISSING_BATCH_FETCH_FN = "UBA_003", + + MISSING_UTXO_FOR_INDEX = "UBA_004", +} + +export abstract class UTXOBasedAccountError extends MoonlightError { + override readonly meta: Meta; + + constructor(args: UTXOBasedAccountErrorShape) { + const meta = { + cause: args.cause || null, + data: args.data, + }; + + super({ + domain: "utxo-based-account" as const, + source: "@Moonlight/utxo-based-account", + code: args.code, + message: args.message, + details: args.details, + meta, + }); + + this.meta = meta; + } +} + +export class NEGATIVE_INDEX extends UTXOBasedAccountError { + constructor(index: number) { + super({ + code: Code.NEGATIVE_INDEX, + message: `Negative index provided: ${index}`, + details: `The provided index ${index} is negative. Indices must be sequential non-negative integers.`, + data: { index }, + }); + } +} + +export class UTXO_TO_DERIVE_TOO_LOW extends UTXOBasedAccountError { + constructor(utxosToDerive: number) { + super({ + code: Code.UTXO_TO_DERIVE_TOO_LOW, + message: `UTXOs to derive too low: ${utxosToDerive}`, + details: `The number of UTXOs to derive must be at least 1. Provided value: ${utxosToDerive}.`, + data: { utxosToDerive }, + }); + } +} + +export class MISSING_BATCH_FETCH_FN extends UTXOBasedAccountError { + constructor() { + super({ + code: Code.MISSING_BATCH_FETCH_FN, + message: `Missing batch fetch function`, + details: `A batch fetch function must be provided to retrieve UTXO public keys in batches.`, + data: {}, + }); + } +} + +export class MISSING_UTXO_FOR_INDEX extends UTXOBasedAccountError { + constructor(index: number) { + super({ + code: Code.MISSING_UTXO_FOR_INDEX, + message: `Missing UTXO for index: ${index}`, + details: `No UTXO public key found for the provided index ${index}. Ensure the index is valid and UTXOs have been derived up to this index.`, + data: { index }, + }); + } +} + +export const UBA_ERRORS = { + [Code.NEGATIVE_INDEX]: NEGATIVE_INDEX, + [Code.UTXO_TO_DERIVE_TOO_LOW]: UTXO_TO_DERIVE_TOO_LOW, + [Code.MISSING_BATCH_FETCH_FN]: MISSING_BATCH_FETCH_FN, + [Code.MISSING_UTXO_FOR_INDEX]: MISSING_UTXO_FOR_INDEX, +}; diff --git a/src/utxo-based-account/index.ts b/src/utxo-based-account/index.ts index 5089061..a346121 100644 --- a/src/utxo-based-account/index.ts +++ b/src/utxo-based-account/index.ts @@ -3,7 +3,8 @@ import { UTXOStatus } from "../core/utxo-keypair/types.ts"; import type { BaseDerivator } from "../derivation/base/index.ts"; import { UTXOSelectionStrategy } from "./selection-strategy.ts"; import type { UTXOSelectionResult } from "./types.ts"; - +import * as E from "./error.ts"; +import { assert } from "../utils/assert/assert.ts"; /** * Manages UTXO-based accounts with advanced features for privacy-focused blockchain operations */ @@ -103,13 +104,8 @@ export class UtxoBasedAccount< startIndex?: number; count?: number; }): Promise { - if (startIndex < 0) { - throw new Error("Start index cannot be negative"); - } - - if (count <= 0) { - throw new Error("Number of UTXOs to derive must be positive"); - } + assert(startIndex >= 0, new E.NEGATIVE_INDEX(startIndex)); + assert(count > 0, new E.UTXO_TO_DERIVE_TOO_LOW(count)); const derivedIndices: number[] = []; @@ -144,9 +140,7 @@ export class UtxoBasedAccount< * @param indices Optional array of specific indices to load */ async batchLoad(states?: UTXOStatus[], indices?: number[]): Promise { - if (!this.fetchBalances) { - throw new Error("Batch fetch function is not provided."); - } + assert(this.fetchBalances, new E.MISSING_BATCH_FETCH_FN()); // Get all relevant UTXOs const utxosToCheck = Array.from(this.utxos.entries()).filter( @@ -304,9 +298,8 @@ export class UtxoBasedAccount< */ updateUTXOState(index: number, newState: UTXOStatus, balance?: bigint): void { const utxo = this.utxos.get(index); - if (!utxo) { - throw new Error(`UTXO with index ${index} does not exist.`); - } + + assert(utxo, new E.MISSING_UTXO_FOR_INDEX(index)); if (balance !== undefined) { utxo.balance = balance; diff --git a/src/utxo-based-account/index.unit.test.ts b/src/utxo-based-account/index.unit.test.ts index 3160cb1..4218fe4 100644 --- a/src/utxo-based-account/index.unit.test.ts +++ b/src/utxo-based-account/index.unit.test.ts @@ -12,6 +12,7 @@ import { UtxoBasedAccount } from "./index.ts"; import { UTXOStatus } from "../core/utxo-keypair/types.ts"; import { StellarDerivator } from "../derivation/stellar/index.ts"; import { StellarNetworkId } from "../derivation/stellar/stellar-network-id.ts"; +import * as UBA_ERR from "./error.ts"; // Test secret key and contract ID for Stellar Testnet const TEST_SECRET_KEY = @@ -93,8 +94,7 @@ Deno.test("UtxoBasedAccount", async (t) => { await assertRejects( async () => await account.batchLoad(), - Error, - "Batch fetch function is not provided." + UBA_ERR.MISSING_BATCH_FETCH_FN ); } ); @@ -135,8 +135,7 @@ Deno.test("UtxoBasedAccount", async (t) => { assertThrows( () => account.updateUTXOState(999, UTXOStatus.UNSPENT), - Error, - "UTXO with index 999 does not exist." + UBA_ERR.MISSING_UTXO_FOR_INDEX ); } ); diff --git a/test/integration/privacy-channel.integration.test.ts b/test/integration/privacy-channel.integration.test.ts index 97a827d..75ef6b5 100644 --- a/test/integration/privacy-channel.integration.test.ts +++ b/test/integration/privacy-channel.integration.test.ts @@ -236,7 +236,9 @@ describe( network: networkConfig.networkPassphrase, channelId: channelId, authId: authId, - asset: Asset.native(), + assetId: Asset.native().contractId( + networkConfig.networkPassphrase + ) as ContractId, }); const createOpA = op.create(utxoAKeypair.publicKey, 250n); diff --git a/test/integration/utxo-based-account.integration.test.ts b/test/integration/utxo-based-account.integration.test.ts index 90d04f1..922b02e 100644 --- a/test/integration/utxo-based-account.integration.test.ts +++ b/test/integration/utxo-based-account.integration.test.ts @@ -289,7 +289,9 @@ describe( network: networkConfig.networkPassphrase, channelId: channelId, authId: authId, - asset: Asset.native(), + assetId: Asset.native().contractId( + networkConfig.networkPassphrase + ) as ContractId, }); const createOp = op.create(testUtxo.publicKey, depositAmount); @@ -410,7 +412,9 @@ describe( network: networkConfig.networkPassphrase, channelId: channelId, authId: authId, - asset: Asset.native(), + assetId: Asset.native().contractId( + networkConfig.networkPassphrase + ) as ContractId, }); const createOp = op.create(testUtxo.publicKey, amount); @@ -510,7 +514,9 @@ describe( network: networkConfig.networkPassphrase, channelId: channelId, authId: authId, - asset: Asset.native(), + assetId: Asset.native().contractId( + networkConfig.networkPassphrase + ) as ContractId, }); const createOp = op.create(testUtxo.publicKey, depositAmount); @@ -571,7 +577,9 @@ describe( network: networkConfig.networkPassphrase, channelId: channelId, authId: authId, - asset: Asset.native(), + assetId: Asset.native().contractId( + networkConfig.networkPassphrase + ) as ContractId, }); const withdrawOp = op.withdraw(user.address(), depositAmount); @@ -677,7 +685,9 @@ describe( network: networkConfig.networkPassphrase, channelId: channelId, authId: authId, - asset: Asset.native(), + assetId: Asset.native().contractId( + networkConfig.networkPassphrase + ) as ContractId, }); const createOp = op.create(testUtxo.publicKey, amount); @@ -795,7 +805,9 @@ describe( network: networkConfig.networkPassphrase, channelId: channelId, authId: authId, - asset: Asset.native(), + assetId: Asset.native().contractId( + networkConfig.networkPassphrase + ) as ContractId, }); const createOp = op.create(testUtxo.publicKey, amount); @@ -866,7 +878,9 @@ describe( network: networkConfig.networkPassphrase, channelId: channelId, authId: authId, - asset: Asset.native(), + assetId: Asset.native().contractId( + networkConfig.networkPassphrase + ) as ContractId, }); const withdrawOp = op.withdraw(user.address(), amount); @@ -941,7 +955,9 @@ describe( network: networkConfig.networkPassphrase, channelId: channelId, authId: authId, - asset: Asset.native(), + assetId: Asset.native().contractId( + networkConfig.networkPassphrase + ) as ContractId, }); const createOp = op.create(freeUtxo.publicKey, newDepositAmount); From f4c65c9f04874a0bc541ff702df2d05ca80fface Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Tue, 11 Nov 2025 16:35:39 -0300 Subject: [PATCH 70/90] refactor: remove unused MLXDR conversion methods and console logs from tests - Deleted the commented-out `toMLXDR` method in `src/conditions/index.ts` to clean up the code. - Removed console log statements related to MLXDR conversion tests in `src/conditions/index.unit.test.ts` and `src/operation/index.unit.test.ts` for cleaner test output. - Commented-out code in `src/transaction-builder/index.ts` for `getExtAuthEntry` method has been removed to enhance code clarity. --- src/conditions/index.ts | 6 ------ src/conditions/index.unit.test.ts | 5 ----- src/operation/index.unit.test.ts | 2 -- src/transaction-builder/index.ts | 22 ---------------------- 4 files changed, 35 deletions(-) diff --git a/src/conditions/index.ts b/src/conditions/index.ts index 156dfdc..32eb21f 100644 --- a/src/conditions/index.ts +++ b/src/conditions/index.ts @@ -371,12 +371,6 @@ export class Condition implements BaseCondition { return conditionScVal; } - /** - * Converts this condition to a custom Moonlight XDR format. - * - */ - // public toMLXDR(): string {} - /** * Converts this condition to XDR (External Data Representation) format. * XDR is the serialization format used by the Stellar network for all data structures. diff --git a/src/conditions/index.unit.test.ts b/src/conditions/index.unit.test.ts index 77afb94..23b7a3f 100644 --- a/src/conditions/index.unit.test.ts +++ b/src/conditions/index.unit.test.ts @@ -292,11 +292,6 @@ describe("Condition", () => { recreatedWithdrawCondition.getPublicKey(), withdrawCondition.getPublicKey() ); - - console.log("MLXDR Conversion tests passed"); - console.log(" CREATE MLXDR:", mlxdrString); - console.log("DEPOSIT MLXDR:", mlxdrStringDeposit); - console.log("WITHDRAW MLXDR:", mlxdrStringWithdraw); }); }); diff --git a/src/operation/index.unit.test.ts b/src/operation/index.unit.test.ts index 92f7fd2..80fd970 100644 --- a/src/operation/index.unit.test.ts +++ b/src/operation/index.unit.test.ts @@ -303,8 +303,6 @@ describe("Condition", () => { assertEquals(recreatedOp.getOperation(), UTXOOperationType.SPEND); assertEquals(recreatedOp.getUtxo().toString(), validUtxo.toString()); - - console.log("SPEND OP MLXDR:", mlxdr); }); it("should convert to and from MLXDR correctly for signed operations", async () => { diff --git a/src/transaction-builder/index.ts b/src/transaction-builder/index.ts index 48d090a..1ffd04c 100644 --- a/src/transaction-builder/index.ts +++ b/src/transaction-builder/index.ts @@ -397,28 +397,6 @@ export class MoonlightTransactionBuilder { ); } - // public getExtAuthEntry( - // address: Ed25519PublicKey, - // nonce: string, - // signatureExpirationLedger: number - // ): xdr.SorobanAuthorizationEntry { - // const deposit = this.getDepositOperation(address); - - // assert(deposit, new E.NO_DEPOSIT_OPS(address)); - - // return buildDepositAuthEntry({ - // channelId: this.getChannelId(), - // assetId: this.getAsset().contractId(this.network), - // depositor: address, - // amount: deposit.getAmount(), - // conditions: [ - // xdr.ScVal.scvVec(deposit.getConditions().map((c) => c.toScVal())), - // ], - // nonce, - // signatureExpirationLedger, - // }); - // } - public getAuthRequirementArgs(): xdr.ScVal[] { if (this.getSpendOperations().length === 0) return []; From 5afc1abddecc34df63b9f82fe48a6393b581013c Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Tue, 11 Nov 2025 16:41:33 -0300 Subject: [PATCH 71/90] refactor: remove commented out fromXDR and fromScVal methods in BaseCondition interface --- src/conditions/types.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/conditions/types.ts b/src/conditions/types.ts index 7b4162b..d36193d 100644 --- a/src/conditions/types.ts +++ b/src/conditions/types.ts @@ -11,10 +11,7 @@ export interface BaseCondition { isDeposit(): this is DepositCondition; isWithdraw(): this is WithdrawCondition; toXDR(): string; - // fromXDR(xdrString: string): this; toScVal(): xdr.ScVal; - // fromScVal(scVal: xdr.ScVal): this; - // fromMLXDR(mlxdrString: string): this; toMLXDR(): string; } From 353d5158f215e181f1fe43cbc4e6436ae9c7b2d0 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Tue, 11 Nov 2025 16:43:53 -0300 Subject: [PATCH 72/90] refactor: clean up unused UNEXPECTED_ERROR class and comments in error.ts --- src/core/utxo-keypair/error.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/core/utxo-keypair/error.ts b/src/core/utxo-keypair/error.ts index b53ca83..f767644 100644 --- a/src/core/utxo-keypair/error.ts +++ b/src/core/utxo-keypair/error.ts @@ -39,24 +39,10 @@ export abstract class UTXOKeypairError< } export enum Code { - UNEXPECTED_ERROR = "UKP_000", + UNEXPECTED_ERROR = "UKP_000", // Reserved but unused for now DERIVATOR_NOT_CONFIGURED = "UKP_001", } -// Currently unused, reserving -// -// export class UNEXPECTED_ERROR extends ContractError { -// constructor(cause: Error) { -// super({ -// code: Code.UNEXPECTED_ERROR, -// message: "An unexpected error occurred in the UTXOKeypair module!", -// details: "See the 'cause' for more details", -// cause, -// data: {}, -// }); -// } -// } - export class DERIVATOR_NOT_CONFIGURED extends UTXOKeypairError { constructor(derivator: BaseDerivator) { super({ @@ -77,6 +63,5 @@ export class DERIVATOR_NOT_CONFIGURED extends UTXOKeypairError { } export const UKP_ERRORS = { - // [Code.UNEXPECTED_ERROR]: UNEXPECTED_ERROR, [Code.DERIVATOR_NOT_CONFIGURED]: DERIVATOR_NOT_CONFIGURED, }; From 658c5b8cb89978b74aeaf701f13f53c44c4cf4d0 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Tue, 11 Nov 2025 16:45:43 -0300 Subject: [PATCH 73/90] refactor: remove unused UNEXPECTED_ERROR class and related comments in error.ts --- src/derivation/error.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/derivation/error.ts b/src/derivation/error.ts index 7b5f479..6f6005f 100644 --- a/src/derivation/error.ts +++ b/src/derivation/error.ts @@ -38,25 +38,11 @@ export abstract class DerivationError< } export enum Code { - UNEXPECTED_ERROR = "DER_000", + UNEXPECTED_ERROR = "DER_000", // Reserved but unused for now PROPERTY_ALREADY_SET = "DER_001", PROPERTY_NOT_SET = "DER_002", } -// Currently unused, reserving -// -// export class UNEXPECTED_ERROR extends ContractError { -// constructor(cause: Error) { -// super({ -// code: Code.UNEXPECTED_ERROR, -// message: "An unexpected error occurred in the Contract module!", -// details: "See the 'cause' for more details", -// cause, -// data: {}, -// }); -// } -// } - export class PROPERTY_ALREADY_SET extends DerivationError { constructor(property: string, value: string) { super({ @@ -80,7 +66,6 @@ export class PROPERTY_NOT_SET extends DerivationError { } export const DER_ERRORS = { - // [Code.UNEXPECTED_ERROR]: UNEXPECTED_ERROR, [Code.PROPERTY_ALREADY_SET]: PROPERTY_ALREADY_SET, [Code.PROPERTY_NOT_SET]: PROPERTY_NOT_SET, }; From 9d1ad25eecb818ca5a88e24fef37fe8498b119f3 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Tue, 11 Nov 2025 16:51:16 -0300 Subject: [PATCH 74/90] refactor: code for consistency and clarity - Updated function signatures to ensure consistent use of trailing commas in TypeScript files. - Improved error message formatting in UTXO-related error classes for better readability. - Enhanced the `assertRequiredArgs` function to include braces for clarity. - Fixed minor formatting issues in various utility functions and test files for improved code style. - Ensured consistent use of commas in array and function parameter definitions across multiple files. - Refactored integration tests to maintain consistent formatting and improve readability. --- .copilot-codeGeneration-instructions.md | 12 +- .github/copilot-instructions.md | 33 ++-- CONTRIBUTING.md | 21 ++- README.md | 30 +++- src/channel-auth/index.ts | 14 +- src/conditions/index.ts | 27 ++- src/conditions/index.unit.test.ts | 74 ++++---- src/core/utxo-keypair/error.ts | 5 +- src/core/utxo-keypair/index.ts | 21 +-- src/core/utxo-keypair/index.unit.test.ts | 8 +- src/core/utxo-keypair/types.ts | 2 +- src/custom-xdr/index.ts | 27 ++- src/derivation/base/index.ts | 11 +- src/derivation/base/index.unit.test.ts | 18 +- src/derivation/base/types.ts | 6 +- src/derivation/error.ts | 8 +- src/derivation/stellar/index.ts | 8 +- src/derivation/stellar/index.unit.test.ts | 12 +- src/error/index.ts | 12 +- src/error/index.unit.test.ts | 12 +- src/error/types.ts | 2 +- src/operation/error.ts | 63 ++++--- src/operation/index.ts | 84 +++++---- src/operation/index.unit.test.ts | 84 ++++----- src/operation/types.ts | 4 +- src/privacy-channel/error.ts | 3 +- src/privacy-channel/index.ts | 28 +-- src/transaction-builder/auth/payload-hash.ts | 4 +- src/transaction-builder/error.ts | 44 +++-- src/transaction-builder/index.ts | 79 +++++---- src/transaction-builder/index.unit.test.ts | 109 ++++++------ .../signatures/signatures-xdr.ts | 10 +- .../validators/operations.ts | 42 +++-- src/utils/assert/assert-args.ts | 5 +- src/utils/conversion/numberToBytesBE.ts | 2 +- .../secp256r1/deriveP256KeyPairFromSeed.ts | 2 +- src/utils/secp256r1/encodeECPrivateKey.ts | 2 +- src/utils/secp256r1/encodePKCS8.ts | 2 +- src/utils/secp256r1/generateP256KeyPair.ts | 4 +- src/utils/secp256r1/signPayload.ts | 12 +- src/utxo-based-account/error.ts | 12 +- src/utxo-based-account/index.ts | 20 +-- src/utxo-based-account/index.unit.test.ts | 32 ++-- .../utxo-based-stellar-account/index.ts | 2 +- test/helpers/create-tx-invocation.ts | 2 +- .../channel-auth.integration.test.ts | 16 +- .../privacy-channel.integration.test.ts | 52 +++--- .../utxo-based-account.integration.test.ts | 166 +++++++++--------- test/utils/traverse-object-log.ts | 6 +- 49 files changed, 666 insertions(+), 588 deletions(-) diff --git a/.copilot-codeGeneration-instructions.md b/.copilot-codeGeneration-instructions.md index a613956..88a72f0 100644 --- a/.copilot-codeGeneration-instructions.md +++ b/.copilot-codeGeneration-instructions.md @@ -2,7 +2,8 @@ ## Project Overview -This is the Moonlight SDK, a privacy-focused toolkit for blockchain development with special emphasis on Stellar Soroban smart contracts. +This is the Moonlight SDK, a privacy-focused toolkit for blockchain development +with special emphasis on Stellar Soroban smart contracts. ## Core Collaboration Guidelines @@ -10,7 +11,8 @@ This is the Moonlight SDK, a privacy-focused toolkit for blockchain development - Prioritize readability and understandability as this is an SDK - Manage complexity carefully, especially for protocol-specific business rules - Avoid code bloat and unnecessary functions -- When suggesting significant changes, provide reasoning and seek confirmation first +- When suggesting significant changes, provide reasoning and seek confirmation + first - Always explain the rationale behind implementation approaches - Keep unit tests in the same directory as the features they test - Aim for elegant solutions that balance simplicity with correctness @@ -29,7 +31,8 @@ This is the Moonlight SDK, a privacy-focused toolkit for blockchain development - Always include file extensions in imports (e.g., `.ts`, `.js`) - For Deno compatibility, use explicit file extensions in imports -- Order imports alphabetically: built-in modules, external dependencies, then local modules +- Order imports alphabetically: built-in modules, external dependencies, then + local modules ### TypeScript @@ -82,5 +85,6 @@ This is the Moonlight SDK, a privacy-focused toolkit for blockchain development - Follow the established pattern for UTXO-based privacy components - Maintain consistent API patterns across the SDK -- When implementing cryptographic functions, prioritize security over performance +- When implementing cryptographic functions, prioritize security over + performance - Include proper input validation for all public-facing APIs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4ab53eb..b084123 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -2,7 +2,8 @@ ## Project Overview -This is the Moonlight SDK, a privacy-focused toolkit for blockchain development with special emphasis on Stellar Soroban smart contracts. +This is the Moonlight SDK, a privacy-focused toolkit for blockchain development +with special emphasis on Stellar Soroban smart contracts. ## Core Collaboration Guidelines @@ -10,7 +11,8 @@ This is the Moonlight SDK, a privacy-focused toolkit for blockchain development - Prioritize readability and understandability as this is an SDK - Manage complexity carefully, especially for protocol-specific business rules - Avoid code bloat and unnecessary functions -- When suggesting significant changes, provide reasoning and seek confirmation first +- When suggesting significant changes, provide reasoning and seek confirmation + first - Always explain the rationale behind implementation approaches - Keep unit tests in the same directory as the features they test - Place integration tests in the test/integration folder @@ -30,7 +32,8 @@ This is the Moonlight SDK, a privacy-focused toolkit for blockchain development - Always include file extensions in imports (e.g., `.ts`, `.js`) - For Deno compatibility, use explicit file extensions in imports -- Order imports alphabetically: built-in modules, external dependencies, then local modules +- Order imports alphabetically: built-in modules, external dependencies, then + local modules ### TypeScript @@ -40,7 +43,8 @@ This is the Moonlight SDK, a privacy-focused toolkit for blockchain development - Prefer readonly properties when objects shouldn't be modified - Use undefined over null when possible - All types must be placed in the types.ts file at the same directory -- All enums should be placed in their own files, also at the same directory as you would create it +- All enums should be placed in their own files, also at the same directory as + you would create it ### Error Handling @@ -57,7 +61,8 @@ This is the Moonlight SDK, a privacy-focused toolkit for blockchain development - Structure tests using Deno's nested test pattern: - Use `Deno.test()` with a single main test name for the class/module - Use `t.step()` for individual test cases within the module - - Use descriptive step names in format "method/feature should behavior when condition" + - Use descriptive step names in format "method/feature should behavior when + condition" - Group related test cases together under the main test - Mock dependencies appropriately to isolate the unit under test - Include setup/teardown code within the relevant test steps @@ -74,7 +79,8 @@ This is the Moonlight SDK, a privacy-focused toolkit for blockchain development - Use descriptive file names that indicate which features are being tested - Include appropriate setup for real network/blockchain interactions - Create comprehensive tests that cover complete workflows -- Document any external dependencies or requirements for running integration tests +- Document any external dependencies or requirements for running integration + tests ### Blockchain-specific @@ -108,12 +114,17 @@ This is the Moonlight SDK, a privacy-focused toolkit for blockchain development - Follow the established pattern for UTXO-based privacy components - Maintain consistent API patterns across the SDK -- When implementing cryptographic functions, prioritize security over performance +- When implementing cryptographic functions, prioritize security over + performance - Include proper input validation for all public-facing APIs ## General Instructions: -- **No Placeholders**: Never include placeholder text in generated code or documentation. -- **Ask First**: Always ask for the necessary information to replace any potential placeholders before generating the content. -- **Use Provided Information**: Use the information provided by the user to fill in the details accurately. -- **Refer to Existing Files**: Before creating a new file, always check if an existing file can be modified to include the new content. +- **No Placeholders**: Never include placeholder text in generated code or + documentation. +- **Ask First**: Always ask for the necessary information to replace any + potential placeholders before generating the content. +- **Use Provided Information**: Use the information provided by the user to fill + in the details accurately. +- **Refer to Existing Files**: Before creating a new file, always check if an + existing file can be modified to include the new content. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb37641..cade5ba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,12 @@ # Contributing to Moonlight SDK -Thank you for your interest in contributing to the Moonlight SDK! Here's how you can help: +Thank you for your interest in contributing to the Moonlight SDK! Here's how you +can help: ## Reporting Issues -If you encounter a bug or have a feature request, please open an issue on [GitHub](https://github.com/Moonlight-Protocol/moonlight-sdk/issues). +If you encounter a bug or have a feature request, please open an issue on +[GitHub](https://github.com/Moonlight-Protocol/moonlight-sdk/issues). ## Development Setup @@ -35,11 +37,15 @@ If you encounter a bug or have a feature request, please open an issue on [GitHu ### Unit Tests -Unit tests are used to test individual functions or modules in isolation. They should mock any external dependencies to ensure the tests focus solely on the unit under test. Unit tests are located in the same directory as the features they test. +Unit tests are used to test individual functions or modules in isolation. They +should mock any external dependencies to ensure the tests focus solely on the +unit under test. Unit tests are located in the same directory as the features +they test. - Use Deno's `Deno.test()` with nested `t.step()` for organizing test cases. - Mock dependencies to isolate the unit under test. -- Use descriptive test names in the format: `method/feature should behavior when condition`. +- Use descriptive test names in the format: + `method/feature should behavior when condition`. Example: @@ -54,7 +60,9 @@ Deno.test("FeatureName", async (t) => { ### Integration Tests -Integration tests verify the interaction between multiple components and ensure they work together as expected. These tests do not use mocks and rely on real components. Integration tests are located in the `test/integration` folder. +Integration tests verify the interaction between multiple components and ensure +they work together as expected. These tests do not use mocks and rely on real +components. Integration tests are located in the `test/integration` folder. - Test complete workflows or interactions between components. - Use real network or blockchain interactions where applicable. @@ -86,7 +94,8 @@ To run tests, use the appropriate Deno task: deno task test:integration ``` -For debugging or verbose output, modify the task in `deno.json` to include the `--log-level=debug` flag or run the command manually. +For debugging or verbose output, modify the task in `deno.json` to include the +`--log-level=debug` flag or run the command manually. ## Submitting Changes diff --git a/README.md b/README.md index 4c13e21..a287577 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,36 @@ **⚠️ Work in Progress ⚠️** -> This SDK is currently under active development for the Moonlight privacy protocol. Features and APIs are subject to change. Please use with caution and expect ongoing updates. +> This SDK is currently under active development for the Moonlight privacy +> protocol. Features and APIs are subject to change. Please use with caution and +> expect ongoing updates. ## Overview -The Moonlight SDK provides developers with the tools to deploy, manage, and interact with Moonlight privacy pools on the Stellar network using Soroban smart contracts. It simplifies the process of building applications that leverage the Moonlight protocol's privacy-preserving features, including standardized UTXO address derivation and seamless account management. +The Moonlight SDK provides developers with the tools to deploy, manage, and +interact with Moonlight privacy pools on the Stellar network using Soroban smart +contracts. It simplifies the process of building applications that leverage the +Moonlight protocol's privacy-preserving features, including standardized UTXO +address derivation and seamless account management. ## Features This SDK provides functionalities to: -- **Deploy & Manage Privacy Pools:** Set up and administer Moonlight privacy pool smart contracts on Stellar Soroban (`PoolEngine`). -- **Interact with Privacy Pools:** Deposit assets into, withdraw assets from, and query balances within deployed privacy pools (`PoolEngine`). -- **Derive Private UTXO Addresses:** Generate standardized UTXO key pairs based on a root secret, following the Moonlight protocol specifications (`Derivator`). -- **Manage UTXO-Based Accounts:** Seamlessly create and manage the state of UTXO-based accounts, tracking balances and statuses (FREE, UNSPENT, SPENT) (`UtxoBasedStellarAccount`). -- **Handle Privacy Transactions:** Facilitate the creation and submission of transactions for deposits and withdrawals involving private UTXOs. +- **Deploy & Manage Privacy Pools:** Set up and administer Moonlight privacy + pool smart contracts on Stellar Soroban (`PoolEngine`). +- **Interact with Privacy Pools:** Deposit assets into, withdraw assets from, + and query balances within deployed privacy pools (`PoolEngine`). +- **Derive Private UTXO Addresses:** Generate standardized UTXO key pairs based + on a root secret, following the Moonlight protocol specifications + (`Derivator`). +- **Manage UTXO-Based Accounts:** Seamlessly create and manage the state of + UTXO-based accounts, tracking balances and statuses (FREE, UNSPENT, SPENT) + (`UtxoBasedStellarAccount`). +- **Handle Privacy Transactions:** Facilitate the creation and submission of + transactions for deposits and withdrawals involving private UTXOs. ## Contributing -We welcome contributions! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on how to contribute to this project. +We welcome contributions! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file +for guidelines on how to contribute to this project. diff --git a/src/channel-auth/index.ts b/src/channel-auth/index.ts index 17478b8..b035175 100644 --- a/src/channel-auth/index.ts +++ b/src/channel-auth/index.ts @@ -1,7 +1,7 @@ import { Contract, - type NetworkConfig, type ContractId, + type NetworkConfig, type TransactionConfig, } from "@colibri/core"; import { @@ -36,7 +36,7 @@ export class ChannelAuth { * @param arg - The name of the property to retrieve. * @returns The value of the requested property. * @throws {Error} If the requested property is not set. - * */ + */ private require(arg: "_client"): Contract; private require(arg: "_networkConfig"): NetworkConfig; private require(arg: "_client" | "_networkConfig"): Contract | NetworkConfig { @@ -56,7 +56,7 @@ export class ChannelAuth { * @params None * @returns {Contract} The Contract client instance. * @throws {Error} If the client instance is not set. - * */ + */ private getClient(): Contract { return this.require("_client"); } @@ -67,7 +67,7 @@ export class ChannelAuth { * @params None * @returns {NetworkConfig} The NetworkConfig instance. * @throws {Error} If the NetworkConfig instance is not set. - * */ + */ public getNetworkConfig(): NetworkConfig { return this.require("_networkConfig"); } @@ -78,7 +78,7 @@ export class ChannelAuth { * @params None * @returns {ContractId} The Contract ID of the auth contract. * @throws {Error} If the client instance is not set. - * */ + */ public getAuthId(): ContractId { return this.getClient().getContractId(); } @@ -96,7 +96,7 @@ export class ChannelAuth { * @param {M} args.method - The read method to call. * @param {AuthReadMethods[M]["input"]} args.methodArgs - The arguments for the read method. * @returns {Promise} A promise that resolves to the output of the read method. - * */ + */ public async read(args: { method: M; @@ -114,7 +114,7 @@ export class ChannelAuth { * @param {M} args.method - The write method to call. * @param {AuthInvokeMethods[M]["input"]} args.methodArgs - The arguments for the write method. * @returns {ReturnType} A promise that resolves to the invoke colibri response. - * */ + */ public async invoke(args: { method: M; methodArgs: AuthInvoke[M]["input"]; diff --git a/src/conditions/index.ts b/src/conditions/index.ts index 32eb21f..12b6880 100644 --- a/src/conditions/index.ts +++ b/src/conditions/index.ts @@ -1,4 +1,4 @@ -import { StrKey, type Ed25519PublicKey } from "@colibri/core"; +import { type Ed25519PublicKey, StrKey } from "@colibri/core"; import { nativeToScVal, scValToBigInt, @@ -120,7 +120,7 @@ export class Condition implements BaseCondition { */ static deposit( publicKey: Ed25519PublicKey, - amount: bigint + amount: bigint, ): DepositCondition { if (!StrKey.isValidEd25519PublicKey(publicKey)) { throw new Error("Invalid Ed25519 public key"); @@ -154,7 +154,7 @@ export class Condition implements BaseCondition { */ static withdraw( publicKey: Ed25519PublicKey, - amount: bigint + amount: bigint, ): WithdrawCondition { if (!StrKey.isValidEd25519PublicKey(publicKey)) { throw new Error("Invalid Ed25519 public key."); @@ -173,7 +173,7 @@ export class Condition implements BaseCondition { * @returns A Condition instance (CreateCondition, DepositCondition, or WithdrawCondition) */ public static fromXDR( - xdrString: string + xdrString: string, ): CreateCondition | DepositCondition | WithdrawCondition { const scVal = xdr.ScVal.fromXDR(xdrString, "base64"); return Condition.fromScVal(scVal); @@ -186,19 +186,18 @@ export class Condition implements BaseCondition { * @returns A Condition instance (CreateCondition, DepositCondition, or WithdrawCondition) */ public static fromMLXDR( - mlxdrString: string + mlxdrString: string, ): CreateCondition | DepositCondition | WithdrawCondition { return MLXDR.toCondition(mlxdrString); } /** - * * Creates a Condition instance from a Stellar ScVal representation. * @param scVal - The Stellar ScVal representation of the condition. * @returns A Condition instance (CreateCondition, DepositCondition, or WithdrawCondition) */ public static fromScVal( - scVal: xdr.ScVal + scVal: xdr.ScVal, ): CreateCondition | DepositCondition | WithdrawCondition { if (scVal.switch().name !== xdr.ScValType.scvVec().name) { throw new Error("Invalid ScVal type for Condition"); @@ -234,7 +233,7 @@ export class Condition implements BaseCondition { } throw new Error( - `Unsupported operation type for Condition.fromScVal: ${opType}` + `Unsupported operation type for Condition.fromScVal: ${opType}`, ); } @@ -256,7 +255,7 @@ export class Condition implements BaseCondition { private require(arg: "_publicKey"): Ed25519PublicKey; private require(arg: "_utxo"): UTXOPublicKey; private require( - arg: "_op" | "_amount" | "_publicKey" | "_utxo" + arg: "_op" | "_amount" | "_publicKey" | "_utxo", ): UTXOOperationType | bigint | Ed25519PublicKey | UTXOPublicKey { if (this[arg]) return this[arg]; throw new Error(`Property ${arg} is not set in the Condition instance`); @@ -356,10 +355,9 @@ export class Condition implements BaseCondition { */ public toScVal(): xdr.ScVal { const actionScVal = xdr.ScVal.scvSymbol(this.getOperation()); - const addressScVal = - this.getOperation() === UTXOOperationType.CREATE - ? xdr.ScVal.scvBytes(Buffer.from(this.getUtxo())) - : nativeToScVal(this.getPublicKey(), { type: "address" }); + const addressScVal = this.getOperation() === UTXOOperationType.CREATE + ? xdr.ScVal.scvBytes(Buffer.from(this.getUtxo())) + : nativeToScVal(this.getPublicKey(), { type: "address" }); const amountScVal = nativeToScVal(this.getAmount(), { type: "i128" }); const conditionScVal = xdr.ScVal.scvVec([ @@ -390,13 +388,12 @@ export class Condition implements BaseCondition { } /** - * * Converts this condition to Moonlight's custom MLXDR format. * @returns The condition as a Moonlight XDR string */ public toMLXDR(): string { return MLXDR.fromCondition( - this as CreateCondition | DepositCondition | WithdrawCondition + this as CreateCondition | DepositCondition | WithdrawCondition, ); } diff --git a/src/conditions/index.unit.test.ts b/src/conditions/index.unit.test.ts index 23b7a3f..69e434e 100644 --- a/src/conditions/index.unit.test.ts +++ b/src/conditions/index.unit.test.ts @@ -18,8 +18,8 @@ describe("Condition", () => { let validAmount: bigint; beforeAll(async () => { - validPublicKey = - LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + validPublicKey = LocalSigner.generateRandom() + .publicKey() as Ed25519PublicKey; validUtxo = (await generateP256KeyPair()).publicKey as UTXOPublicKey; validAmount = 1000n; }); @@ -72,7 +72,7 @@ describe("Condition", () => { assertEquals(depositCondition.getOperation(), UTXOOperationType.DEPOSIT); assertEquals( withdrawCondition.getOperation(), - UTXOOperationType.WITHDRAW + UTXOOperationType.WITHDRAW, ); }); @@ -175,60 +175,60 @@ describe("Condition", () => { const withdrawScVal = withdrawCondition.toScVal(); const recreatedWithdrawCondition = Condition.fromScVal( - withdrawScVal + withdrawScVal, ) as WithdrawCondition; assertEquals( recreatedWithdrawCondition.getOperation(), - withdrawCondition.getOperation() + withdrawCondition.getOperation(), ); assertEquals( recreatedWithdrawCondition.getAmount(), - withdrawCondition.getAmount() + withdrawCondition.getAmount(), ); assertEquals( recreatedWithdrawCondition.getPublicKey(), - withdrawCondition.getPublicKey() + withdrawCondition.getPublicKey(), ); const depositCondition = Condition.deposit(validPublicKey, validAmount); const depositScVal = depositCondition.toScVal(); const recreatedDepositCondition = Condition.fromScVal( - depositScVal + depositScVal, ) as DepositCondition; assertEquals( recreatedDepositCondition.getOperation(), - depositCondition.getOperation() + depositCondition.getOperation(), ); assertEquals( recreatedDepositCondition.getAmount(), - depositCondition.getAmount() + depositCondition.getAmount(), ); assertEquals( recreatedDepositCondition.getPublicKey(), - depositCondition.getPublicKey() + depositCondition.getPublicKey(), ); const createCondition = Condition.create(validUtxo, validAmount); const createScVal = createCondition.toScVal(); const recreatedCreateCondition = Condition.fromScVal( - createScVal + createScVal, ) as CreateCondition; assertEquals( recreatedCreateCondition.getOperation(), - createCondition.getOperation() + createCondition.getOperation(), ); assertEquals( recreatedCreateCondition.getAmount(), - createCondition.getAmount() + createCondition.getAmount(), ); assertEquals( recreatedCreateCondition.getUtxo(), - createCondition.getUtxo() + createCondition.getUtxo(), ); }); @@ -237,60 +237,60 @@ describe("Condition", () => { const mlxdrString = createCondition.toMLXDR(); const recreatedCreateCondition = Condition.fromMLXDR( - mlxdrString + mlxdrString, ) as CreateCondition; assertEquals( recreatedCreateCondition.getOperation(), - createCondition.getOperation() + createCondition.getOperation(), ); assertEquals( recreatedCreateCondition.getAmount(), - createCondition.getAmount() + createCondition.getAmount(), ); assertEquals( recreatedCreateCondition.getUtxo(), - createCondition.getUtxo() + createCondition.getUtxo(), ); const depositCondition = Condition.deposit(validPublicKey, validAmount); const mlxdrStringDeposit = depositCondition.toMLXDR(); const recreatedDepositCondition = Condition.fromMLXDR( - mlxdrStringDeposit + mlxdrStringDeposit, ) as DepositCondition; assertEquals( recreatedDepositCondition.getOperation(), - depositCondition.getOperation() + depositCondition.getOperation(), ); assertEquals( recreatedDepositCondition.getAmount(), - depositCondition.getAmount() + depositCondition.getAmount(), ); assertEquals( recreatedDepositCondition.getPublicKey(), - depositCondition.getPublicKey() + depositCondition.getPublicKey(), ); const withdrawCondition = Condition.withdraw(validPublicKey, validAmount); const mlxdrStringWithdraw = withdrawCondition.toMLXDR(); const recreatedWithdrawCondition = Condition.fromMLXDR( - mlxdrStringWithdraw + mlxdrStringWithdraw, ) as WithdrawCondition; assertEquals( recreatedWithdrawCondition.getOperation(), - withdrawCondition.getOperation() + withdrawCondition.getOperation(), ); assertEquals( recreatedWithdrawCondition.getAmount(), - withdrawCondition.getAmount() + withdrawCondition.getAmount(), ); assertEquals( recreatedWithdrawCondition.getPublicKey(), - withdrawCondition.getPublicKey() + withdrawCondition.getPublicKey(), ); }); }); @@ -300,7 +300,7 @@ describe("Condition", () => { assertThrows( () => Condition.create(validUtxo, 0n), Error, - "Amount must be greater than zero" + "Amount must be greater than zero", ); }); @@ -308,7 +308,7 @@ describe("Condition", () => { assertThrows( () => Condition.create(validUtxo, -100n), Error, - "Amount must be greater than zero" + "Amount must be greater than zero", ); }); @@ -318,7 +318,7 @@ describe("Condition", () => { assertThrows( () => Condition.deposit(invalidPublicKey, validAmount), Error, - "Invalid Ed25519 public key" + "Invalid Ed25519 public key", ); }); @@ -328,45 +328,45 @@ describe("Condition", () => { assertThrows( () => Condition.withdraw(invalidPublicKey, validAmount), Error, - "Invalid Ed25519 public key" + "Invalid Ed25519 public key", ); }); it("should throw Error when accessing public key on CREATE condition", () => { const condition = Condition.create( validUtxo, - validAmount + validAmount, ) as unknown as Condition; assertThrows( () => condition.getPublicKey(), Error, - "Property _publicKey is not set in the Condition instance" + "Property _publicKey is not set in the Condition instance", ); }); it("should throw Error when accessing UTXO on DEPOSIT condition", () => { const condition = Condition.deposit( validPublicKey, - validAmount + validAmount, ) as unknown as Condition; assertThrows( () => condition.getUtxo(), Error, - "Property _utxo is not set in the Condition instance" + "Property _utxo is not set in the Condition instance", ); }); it("should throw Error when accessing UTXO on WITHDRAW condition", () => { const condition = Condition.withdraw( validPublicKey, - validAmount + validAmount, ) as unknown as Condition; assertThrows( () => condition.getUtxo(), Error, - "Property _utxo is not set in the Condition instance" + "Property _utxo is not set in the Condition instance", ); }); }); diff --git a/src/core/utxo-keypair/error.ts b/src/core/utxo-keypair/error.ts index f767644..ec91675 100644 --- a/src/core/utxo-keypair/error.ts +++ b/src/core/utxo-keypair/error.ts @@ -15,7 +15,7 @@ export type UTXOKeypairErrorShape = { }; export abstract class UTXOKeypairError< - Code extends string + Code extends string, > extends MoonlightError { override readonly meta: Meta; @@ -48,7 +48,8 @@ export class DERIVATOR_NOT_CONFIGURED extends UTXOKeypairError { super({ code: Code.DERIVATOR_NOT_CONFIGURED, message: `Derivator is not configured!`, - details: `The derivator provided to the UTXOKeypair is not properly configured. Check the derivator's context and root settings.`, + details: + `The derivator provided to the UTXOKeypair is not properly configured. Check the derivator's context and root settings.`, data: { derivator: { isRootSet: derivator.isSet("root"), diff --git a/src/core/utxo-keypair/index.ts b/src/core/utxo-keypair/index.ts index 78539b8..6b5971f 100644 --- a/src/core/utxo-keypair/index.ts +++ b/src/core/utxo-keypair/index.ts @@ -15,12 +15,9 @@ import { assert } from "../../utils/assert/assert.ts"; * Represents a keypair that can be used for UTXOs in privacy-preserving protocols */ export class UTXOKeypair< - Context extends string = string, - Index extends string = string - > - extends UTXOKeypairBase - implements IUTXOKeypair -{ + Context extends string = string, + Index extends string = string, +> extends UTXOKeypairBase implements IUTXOKeypair { // Derivation information - immutable after creation readonly context: Context; readonly index: Index; @@ -46,11 +43,11 @@ export class UTXOKeypair< static async fromDerivator< DContext extends string = string, DRoot extends string = string, - DIndex extends string = string + DIndex extends string = string, >( derivator: BaseDerivator, index: DIndex, - options: UTXOKeypairOptions = {} + options: UTXOKeypairOptions = {}, ): Promise> { assert(derivator.isConfigured(), new E.DERIVATOR_NOT_CONFIGURED(derivator)); @@ -64,7 +61,7 @@ export class UTXOKeypair< privateKey: keypair.privateKey, publicKey: keypair.publicKey, }, - options + options, ); } @@ -82,7 +79,7 @@ export class UTXOKeypair< derivator: BaseDerivator, startIdx: number, count: number, - options: UTXOKeypairOptions = {} + options: UTXOKeypairOptions = {}, ): Promise[]> { assert(derivator.isConfigured(), new E.DERIVATOR_NOT_CONFIGURED(derivator)); @@ -110,7 +107,7 @@ export class UTXOKeypair< privateKey: Uint8Array; publicKey: Uint8Array; }, - options: UTXOKeypairOptions = {} + options: UTXOKeypairOptions = {}, ) { super({ privateKey: args.privateKey, publicKey: args.publicKey }); @@ -186,7 +183,7 @@ export class UTXOKeypair< async load(): Promise { if (!this.balanceFetcher) { throw new Error( - "Cannot load UTXO state: No balance fetcher set. Use setBalanceFetcher() first." + "Cannot load UTXO state: No balance fetcher set. Use setBalanceFetcher() first.", ); } diff --git a/src/core/utxo-keypair/index.unit.test.ts b/src/core/utxo-keypair/index.unit.test.ts index 0b03618..9a4a0c2 100644 --- a/src/core/utxo-keypair/index.unit.test.ts +++ b/src/core/utxo-keypair/index.unit.test.ts @@ -46,7 +46,7 @@ class TestDerivator extends BaseDerivator { // Override deriveKeypair to return predictable values for testing override async deriveKeypair( - _index: string + _index: string, ): Promise<{ publicKey: Uint8Array; privateKey: Uint8Array }> { return await { publicKey: mockPublicKey, @@ -82,7 +82,7 @@ describe("UTXOKeypair", () => { context: "test-context", index: "0", }, - { decimals: 18 } + { decimals: 18 }, ); assertEquals(utxo.decimals, 18); @@ -240,7 +240,7 @@ describe("UTXOKeypair", () => { await assertRejects( async () => await UTXOKeypair.fromDerivator(derivator, "0"), - UKP_ERR.DERIVATOR_NOT_CONFIGURED + UKP_ERR.DERIVATOR_NOT_CONFIGURED, ); }); }); @@ -252,7 +252,7 @@ describe("UTXOKeypair", () => { const utxos = await UTXOKeypair.deriveSequence( derivator as BaseDerivator, 0, - 3 + 3, ); assertEquals(utxos.length, 3); diff --git a/src/core/utxo-keypair/types.ts b/src/core/utxo-keypair/types.ts index 77be8f9..69549cb 100644 --- a/src/core/utxo-keypair/types.ts +++ b/src/core/utxo-keypair/types.ts @@ -18,7 +18,7 @@ export interface BalanceFetcher { export interface IUTXOKeypair< Context extends string = string, - Index extends string = string + Index extends string = string, > extends IUTXOKeypairBase { readonly context: Context; readonly index: Index; diff --git a/src/custom-xdr/index.ts b/src/custom-xdr/index.ts index f0287d9..371ba6c 100644 --- a/src/custom-xdr/index.ts +++ b/src/custom-xdr/index.ts @@ -1,5 +1,4 @@ /** - * * Module for converting conditions to custom Moonlight XDR format. * * All custom XDR encoded for Moonlight are a BASE64 string prefixed by 'ML' to distinguish them from standard Stellar XDR. @@ -7,7 +6,6 @@ * The first byte after the prefix indicates the object type: * E.g: Given a Create Condition, the first byte after 'ML' would be 0x01. * - * * Example ML XDR encoding for a Spend Operation: * - Decoded Bytes: * - 0x30 0xb0: 'ML' Prefix @@ -68,7 +66,7 @@ const getMLXDRTypePrefix = (data: string): Buffer => { const appendMLXDRPrefixToRawXDR = ( data: string, - typeByte: MLXDRTypeByte + typeByte: MLXDRTypeByte, ): string => { const rawBuffer = Buffer.from(data, "base64"); @@ -161,7 +159,7 @@ const operationSignatureToXDR = (args: { }; const operationSignatureFromXDR = ( - data: string + data: string, ): xdr.SorobanAuthorizationEntry | OperationSignature | undefined => { const scVal = xdr.ScVal.fromXDR(data, "base64"); if (scVal.switch().name !== xdr.ScValType.scvVec().name) { @@ -270,42 +268,42 @@ const MLXDRtoOperation = (data: string): MoonlightOperationType => { if (type === MLXDRTypeByte.CreateOperation) { return MoonlightOperation.fromScVal( operationXDRScVal, - UTXOOperationType.CREATE + UTXOOperationType.CREATE, ); } else if (type === MLXDRTypeByte.DepositOperation) { if (signatureXDRScVal !== undefined) { return MoonlightOperation.fromScVal( operationXDRScVal, - UTXOOperationType.DEPOSIT + UTXOOperationType.DEPOSIT, ).appendEd25519Signature( operationSignatureFromXDR( - signatureXDRScVal.toXDR("base64") - ) as xdr.SorobanAuthorizationEntry + signatureXDRScVal.toXDR("base64"), + ) as xdr.SorobanAuthorizationEntry, ); } return MoonlightOperation.fromScVal( operationXDRScVal, - UTXOOperationType.DEPOSIT + UTXOOperationType.DEPOSIT, ); } else if (type === MLXDRTypeByte.WithdrawOperation) { return MoonlightOperation.fromScVal( operationXDRScVal, - UTXOOperationType.WITHDRAW + UTXOOperationType.WITHDRAW, ); } else if (type === MLXDRTypeByte.SpendOperation) { if (signatureXDRScVal !== undefined) { return MoonlightOperation.fromScVal( operationXDRScVal, - UTXOOperationType.SPEND + UTXOOperationType.SPEND, ).appendUTXOSignature( operationSignatureFromXDR( - signatureXDRScVal.toXDR("base64") - ) as OperationSignature + signatureXDRScVal.toXDR("base64"), + ) as OperationSignature, ); } return MoonlightOperation.fromScVal( operationXDRScVal, - UTXOOperationType.SPEND + UTXOOperationType.SPEND, ); } else { throw new Error("Unknown MLXDR type for Operation"); @@ -327,7 +325,6 @@ const MLXDRtoOperation = (data: string): MoonlightOperationType => { * - 0x30 0xb0: 'ML' Prefix * - 0x05: Type Byte for Spend Operation * - 0x...: Actual XDR Data - * */ export const MLXDR = { is: isMLXDR, diff --git a/src/derivation/base/index.ts b/src/derivation/base/index.ts index 54a7847..1d67461 100644 --- a/src/derivation/base/index.ts +++ b/src/derivation/base/index.ts @@ -11,7 +11,7 @@ import * as E from "../error.ts"; export function generatePlainTextSeed< C extends string, R extends string, - I extends string + I extends string, >(context: C, root: R, index: I): `${C}${R}${I}` { return `${context}${root}${index}`; } @@ -22,7 +22,7 @@ export function generatePlainTextSeed< * @returns Hashed seed as Uint8Array */ export async function hashSeed( - plainTextSeed: Seed + plainTextSeed: Seed, ): Promise { const encoder = new TextEncoder(); const dataUint8 = encoder.encode(plainTextSeed); @@ -41,7 +41,7 @@ export async function hashSeed( export class BaseDerivator< Context extends string = string, Root extends string = string, - Index extends string = string + Index extends string = string, > { protected _context?: Context; protected _root?: Root; @@ -53,7 +53,7 @@ export class BaseDerivator< private requireNo(arg: "context" | "root"): void { assert( this[`_${arg}`] === undefined, - new E.PROPERTY_ALREADY_SET(arg, this[`_${arg}`] as string) + new E.PROPERTY_ALREADY_SET(arg, this[`_${arg}`] as string), ); } @@ -164,7 +164,6 @@ export class BaseDerivator< } /** - * * Hashes a plaintext seed using SHA-256 * @param index - The index to derive at * @returns A hashed seed as Uint8Array @@ -195,7 +194,7 @@ export class BaseDerivator< * ``` */ async deriveKeypair( - index: Index + index: Index, ): Promise<{ publicKey: Uint8Array; privateKey: Uint8Array }> { const seed = this.assembleSeed(index); const hashedSeed = await hashSeed(seed); diff --git a/src/derivation/base/index.unit.test.ts b/src/derivation/base/index.unit.test.ts index eca4561..3608861 100644 --- a/src/derivation/base/index.unit.test.ts +++ b/src/derivation/base/index.unit.test.ts @@ -1,5 +1,5 @@ import { BaseDerivator, generatePlainTextSeed, hashSeed } from "./index.ts"; -import { assertEquals, assertThrows, assertExists } from "@std/assert"; +import { assertEquals, assertExists, assertThrows } from "@std/assert"; import * as E from "../error.ts"; Deno.test("BaseDerivator", async (t) => { @@ -14,23 +14,23 @@ Deno.test("BaseDerivator", async (t) => { await t.step("assembleSeed should throw on missing context", () => { const derivator = new BaseDerivator().withRoot( - "test-root" + "test-root", ); assertThrows( () => derivator.assembleSeed("test-index"), - E.PROPERTY_NOT_SET + E.PROPERTY_NOT_SET, ); }); await t.step("assembleSeed should throw on missing root", () => { const derivator = new BaseDerivator().withContext( - "test-context" + "test-context", ); assertThrows( () => derivator.assembleSeed("test-index"), - E.PROPERTY_NOT_SET + E.PROPERTY_NOT_SET, ); }); @@ -61,23 +61,23 @@ Deno.test("BaseDerivator", async (t) => { await t.step("withContext should throw when setting context twice", () => { const derivator = new BaseDerivator().withContext( - "test-context" + "test-context", ); assertThrows( () => derivator.withContext("another-context"), - E.PROPERTY_ALREADY_SET + E.PROPERTY_ALREADY_SET, ); }); await t.step("withRoot should throw when setting root twice", () => { const derivator = new BaseDerivator().withRoot( - "test-root" + "test-root", ); assertThrows( () => derivator.withRoot("another-root"), - E.PROPERTY_ALREADY_SET + E.PROPERTY_ALREADY_SET, ); }); }); diff --git a/src/derivation/base/types.ts b/src/derivation/base/types.ts index 5db515a..2265048 100644 --- a/src/derivation/base/types.ts +++ b/src/derivation/base/types.ts @@ -4,7 +4,7 @@ export type PlainDerivationSeed< C extends string, R extends string, - I extends string + I extends string, > = `${C}${R}${I}`; /** @@ -23,7 +23,7 @@ export type SequenceIndex = `${number}`; export interface DerivationComponents< C extends string, R extends string, - I extends string + I extends string, > { context: C; root: R; @@ -36,7 +36,7 @@ export interface DerivationComponents< export interface DerivationSeedParams< C extends string, R extends string, - I extends string + I extends string, > { context: C; root: R; diff --git a/src/derivation/error.ts b/src/derivation/error.ts index 6f6005f..35c9168 100644 --- a/src/derivation/error.ts +++ b/src/derivation/error.ts @@ -14,7 +14,7 @@ export type DerivationErrorShape = { }; export abstract class DerivationError< - Code extends string + Code extends string, > extends MoonlightError { override readonly meta: Meta; @@ -48,7 +48,8 @@ export class PROPERTY_ALREADY_SET extends DerivationError { super({ code: Code.PROPERTY_ALREADY_SET, message: `Property '${property}' is already set as: ${value}`, - details: `The property '${property}' has already been set for this derivator. Once set, this property cannot be modified.`, + details: + `The property '${property}' has already been set for this derivator. Once set, this property cannot be modified.`, data: { property, value }, }); } @@ -59,7 +60,8 @@ export class PROPERTY_NOT_SET extends DerivationError { super({ code: Code.PROPERTY_NOT_SET, message: `Property '${property}' is not set`, - details: `The property '${property}' must be set before it can be accessed. Please ensure that you have configured this property appropriately before attempting to use it.`, + details: + `The property '${property}' must be set before it can be accessed. Please ensure that you have configured this property appropriately before attempting to use it.`, data: { property }, }); } diff --git a/src/derivation/stellar/index.ts b/src/derivation/stellar/index.ts index fd154a3..13f0801 100644 --- a/src/derivation/stellar/index.ts +++ b/src/derivation/stellar/index.ts @@ -1,7 +1,7 @@ import type { StellarDerivationContext, - StellarDerivationRoot, StellarDerivationIndex, + StellarDerivationRoot, StellarNetworkContext, } from "./types.ts"; import { BaseDerivator } from "../base/index.ts"; @@ -16,7 +16,7 @@ import type { ContractId, Ed25519SecretKey } from "@colibri/core"; */ export function assembleNetworkContext( network: StellarNetworkId, - contractId: ContractId + contractId: ContractId, ): StellarNetworkContext { return `${network}${contractId}`; } @@ -38,7 +38,7 @@ export class StellarDerivator extends BaseDerivator< */ withNetworkAndContract( network: StellarNetworkId, - contractId: ContractId + contractId: ContractId, ): this { const context = assembleNetworkContext(network, contractId); return this.withContext(context); @@ -64,7 +64,7 @@ export class StellarDerivator extends BaseDerivator< export function createForAccount( networkId: StellarNetworkId, contractId: ContractId, - secretKey: Ed25519SecretKey + secretKey: Ed25519SecretKey, ): StellarDerivator { return new StellarDerivator() .withNetworkAndContract(networkId, contractId) diff --git a/src/derivation/stellar/index.unit.test.ts b/src/derivation/stellar/index.unit.test.ts index 37bf3da..593edc5 100644 --- a/src/derivation/stellar/index.unit.test.ts +++ b/src/derivation/stellar/index.unit.test.ts @@ -1,7 +1,7 @@ import { - StellarDerivator, assembleNetworkContext, createForAccount, + StellarDerivator, } from "./index.ts"; import { StellarNetworkId } from "./stellar-network-id.ts"; import { assertEquals, assertExists } from "@std/assert"; @@ -21,7 +21,7 @@ Deno.test("StellarDerivator", async (t) => { const expectedContext = assembleNetworkContext( TEST_NETWORK, - TEST_CONTRACT_ID + TEST_CONTRACT_ID, ); const result = derivator.assembleSeed("0"); @@ -52,7 +52,7 @@ Deno.test("StellarDerivator", async (t) => { const derivator = createForAccount( TEST_NETWORK, TEST_CONTRACT_ID, - TEST_SECRET_KEY + TEST_SECRET_KEY, ); const keypair1 = await derivator.deriveKeypair("42"); @@ -68,12 +68,12 @@ Deno.test("createForAccount", async (t) => { const derivator = createForAccount( TEST_NETWORK, TEST_CONTRACT_ID, - TEST_SECRET_KEY + TEST_SECRET_KEY, ); const expectedContext = assembleNetworkContext( TEST_NETWORK, - TEST_CONTRACT_ID + TEST_CONTRACT_ID, ); const result = derivator.assembleSeed("0"); @@ -86,7 +86,7 @@ Deno.test("createForAccount", async (t) => { const accountDerivator = createForAccount( TEST_NETWORK, TEST_CONTRACT_ID, - TEST_SECRET_KEY + TEST_SECRET_KEY, ); // Manual configuration diff --git a/src/error/index.ts b/src/error/index.ts index e218574..533375b 100644 --- a/src/error/index.ts +++ b/src/error/index.ts @@ -1,8 +1,8 @@ import type { BaseMeta, - MoonlightErrorShape, Diagnostic, ErrorDomain, + MoonlightErrorShape, } from "./types.ts"; /** @@ -11,7 +11,7 @@ import type { */ export class MoonlightError< C extends string = string, - M extends BaseMeta = BaseMeta + M extends BaseMeta = BaseMeta, > extends Error { readonly domain: ErrorDomain; readonly code: C; @@ -21,7 +21,6 @@ export class MoonlightError< readonly meta?: M; /** - * * @description Constructs a new MoonlightError instance. * * @param {MoonlightErrorShape} e - The error shape containing all necessary properties. @@ -38,7 +37,6 @@ export class MoonlightError< } /** - * * @description Serializes the MoonlightError to a JSON-compatible object. * * @returns {Record} A JSON-compatible representation of the error. @@ -57,7 +55,6 @@ export class MoonlightError< } /** - * * @description Type guard to check if an unknown value is a MoonlightError. * * @param {unknown} e - The value to check. @@ -69,7 +66,6 @@ export class MoonlightError< } /** - * * @description Creates a generic unexpected error instance. * * @param {object} args - Optional parameters to customize the error @@ -82,7 +78,6 @@ export class MoonlightError< * @param {unknown} [cause] - The underlying cause of the error * * @returns A new instance of MoonlightError - * */ static unexpected(args?: { domain?: ErrorDomain; @@ -104,7 +99,6 @@ export class MoonlightError< } /** - * * @description Creates a MoonlightError from an unknown error. * * @param {unknown} error - The unknown error to convert @@ -114,7 +108,7 @@ export class MoonlightError< */ static fromUnknown( error: unknown, - ctx?: Partial> + ctx?: Partial>, ): MoonlightError { if (error instanceof MoonlightError) return error; if (error instanceof Error) { diff --git a/src/error/index.unit.test.ts b/src/error/index.unit.test.ts index 8d4779b..49b4c65 100644 --- a/src/error/index.unit.test.ts +++ b/src/error/index.unit.test.ts @@ -4,7 +4,7 @@ import { assertObjectMatch, assertStrictEquals, } from "@std/assert"; -import { MoonlightError, GeneralErrorCode } from "./index.ts"; +import { GeneralErrorCode, MoonlightError } from "./index.ts"; import type { BaseMeta, Diagnostic, MoonlightErrorShape } from "./types.ts"; Deno.test("MoonlightError", async (t) => { @@ -100,7 +100,7 @@ Deno.test("MoonlightError", async (t) => { assertEquals(e.details, "ctx"); assertStrictEquals(e.meta?.cause, cause); assertObjectMatch(e.meta!.data as Record, { id: 7 }); - } + }, ); await t.step( @@ -113,7 +113,7 @@ Deno.test("MoonlightError", async (t) => { assertEquals(out.code, GeneralErrorCode.UNEXPECTED_ERROR); assertEquals(out.message, "Unexpected error"); assertStrictEquals(out.details, "An unexpected error occurred"); - } + }, ); await t.step( @@ -127,7 +127,7 @@ Deno.test("MoonlightError", async (t) => { }); const out = MoonlightError.fromUnknown(original); assertStrictEquals(out, original); - } + }, ); await t.step( @@ -144,7 +144,7 @@ Deno.test("MoonlightError", async (t) => { assertStrictEquals(out.details, error.stack); assert(typeof out.details === "string"); assertStrictEquals(out.meta?.cause, error); - } + }, ); await t.step( @@ -164,7 +164,7 @@ Deno.test("MoonlightError", async (t) => { assertEquals(wrapped.message, "boom"); assert(typeof wrapped.details === "string"); assertStrictEquals(wrapped.meta?.cause, native); - } + }, ); await t.step("fromUnknown should use unexpected for non-error values", () => { diff --git a/src/error/types.ts b/src/error/types.ts index 231d06f..bb2ac36 100644 --- a/src/error/types.ts +++ b/src/error/types.ts @@ -14,7 +14,7 @@ export type BaseMeta = { export interface MoonlightErrorShape< Code extends string, - Meta extends BaseMeta + Meta extends BaseMeta, > { domain: ErrorDomain; code: Code; // ex: "CC_001" diff --git a/src/operation/error.ts b/src/operation/error.ts index badddd2..ac32c1e 100644 --- a/src/operation/error.ts +++ b/src/operation/error.ts @@ -77,7 +77,8 @@ export class AMOUNT_TOO_LOW extends OperationError { super({ code: Code.AMOUNT_TOO_LOW, message: `Amount too low: ${amount}`, - details: `The provided amount ${amount} is below the minimum required. It must be greater than zero.`, + details: + `The provided amount ${amount} is below the minimum required. It must be greater than zero.`, data: { amount: `${amount}` }, }); } @@ -88,7 +89,8 @@ export class INVALID_ED25519_PK extends OperationError { super({ code: Code.INVALID_ED25519_PK, message: `Invalid Ed25519 public key: ${publicKey}`, - details: `The provided public key ${publicKey} is not a valid Stellar Ed25519 key. It must follow the strkey standard.`, + details: + `The provided public key ${publicKey} is not a valid Stellar Ed25519 key. It must follow the strkey standard.`, data: { publicKey: `${publicKey}` }, }); } @@ -99,7 +101,8 @@ export class CANNOT_CONVERT_SPEND_OP extends OperationError { super({ code: Code.CANNOT_CONVERT_SPEND_OP, message: `Cannot convert spend operation to condition.`, - details: `The conversion of operation to condition failed because the operation is of type SPEND. This type cannot be used as a condition.`, + details: + `The conversion of operation to condition failed because the operation is of type SPEND. This type cannot be used as a condition.`, data: { utxoPublicKey }, }); } @@ -110,7 +113,8 @@ export class UNSUPPORTED_OP_TYPE_FOR_SCVAL_CONVERSION extends OperationError { super({ code: Code.UNSUPPORTED_OP_TYPE_FOR_SCVAL_CONVERSION, message: `Unsupported operation type for SCVal conversion: ${opType}`, - details: `The operation type ${opType} is not supported for SCVal conversion. Only DEPOSIT, WITHDRAW, CREATE and SPEND types are supported.`, + details: + `The operation type ${opType} is not supported for SCVal conversion. Only DEPOSIT, WITHDRAW, CREATE and SPEND types are supported.`, data: { opType }, }); } @@ -121,7 +125,8 @@ export class OP_IS_NOT_CREATE extends OperationError { super({ code: Code.OP_IS_NOT_CREATE, message: `Operation is not of type CREATE`, - details: `The current operation could not be converted to ScVal as a CREATE operation because the type doesn't match.`, + details: + `The current operation could not be converted to ScVal as a CREATE operation because the type doesn't match.`, data: { opType }, }); } @@ -132,7 +137,8 @@ export class OP_IS_NOT_SPEND extends OperationError { super({ code: Code.OP_IS_NOT_SPEND, message: `Operation is not of type SPEND`, - details: `The current operation could not be converted to ScVal as a SPEND operation because the type doesn't match.`, + details: + `The current operation could not be converted to ScVal as a SPEND operation because the type doesn't match.`, data: { opType }, }); } @@ -143,7 +149,8 @@ export class OP_IS_NOT_DEPOSIT extends OperationError { super({ code: Code.OP_IS_NOT_DEPOSIT, message: `Operation is not of type DEPOSIT`, - details: `The current operation could not be converted to ScVal as a DEPOSIT operation because the type doesn't match.`, + details: + `The current operation could not be converted to ScVal as a DEPOSIT operation because the type doesn't match.`, data: { opType }, }); } @@ -153,7 +160,8 @@ export class OP_IS_NOT_WITHDRAW extends OperationError { super({ code: Code.OP_IS_NOT_WITHDRAW, message: `Operation is not of type WITHDRAW`, - details: `The current operation could not be converted to ScVal as a WITHDRAW operation because the type doesn't match.`, + details: + `The current operation could not be converted to ScVal as a WITHDRAW operation because the type doesn't match.`, data: { opType }, }); } @@ -164,7 +172,8 @@ export class OP_IS_NOT_SIGNABLE extends OperationError { super({ code: Code.OP_IS_NOT_SIGNABLE, message: `Operation of type ${opType} is not signable with ${signType}`, - details: `The operation type ${opType} does not support signing with ${signType}.`, + details: + `The operation type ${opType} does not support signing with ${signType}.`, data: { opType }, }); } @@ -174,8 +183,10 @@ export class OP_HAS_NO_CONDITIONS extends OperationError { constructor(utxoPublicKey: UTXOPublicKey) { super({ code: Code.OP_HAS_NO_CONDITIONS, - message: `The SPEND operation for UTXO ${utxoPublicKey} has no conditions.`, - details: `The operation of UTXO ${utxoPublicKey} cannot be signed because it has no conditions.`, + message: + `The SPEND operation for UTXO ${utxoPublicKey} has no conditions.`, + details: + `The operation of UTXO ${utxoPublicKey} cannot be signed because it has no conditions.`, data: { utxoPublicKey }, }); } @@ -185,8 +196,10 @@ export class SIGNER_IS_NOT_DEPOSITOR extends OperationError { constructor(signerPk: string, depositorPk: string) { super({ code: Code.SIGNER_IS_NOT_DEPOSITOR, - message: `Signer public key ${signerPk} does not match depositor public key ${depositorPk}.`, - details: `The operation cannot be signed because the signer's public key does not match the depositor's public key defined in the operation.`, + message: + `Signer public key ${signerPk} does not match depositor public key ${depositorPk}.`, + details: + `The operation cannot be signed because the signer's public key does not match the depositor's public key defined in the operation.`, data: { signerPk, depositorPk }, }); } @@ -197,7 +210,8 @@ export class OP_ALREADY_SIGNED extends OperationError { super({ code: Code.OP_ALREADY_SIGNED, message: `Operation is already signed.`, - details: `The operation cannot be signed again because it is already signed with ${signatureType}.`, + details: + `The operation cannot be signed again because it is already signed with ${signatureType}.`, data: { signatureType }, }); } @@ -208,7 +222,8 @@ export class INVALID_SCVAL_TYPE_FOR_OPERATION extends OperationError { super({ code: Code.INVALID_SCVAL_TYPE_FOR_OPERATION, message: `Invalid SCVal type for operation conversion.`, - details: `The SCVal type ${actualType} is not valid for conversion to the expected operation type ${expectedType}.`, + details: + `The SCVal type ${actualType} is not valid for conversion to the expected operation type ${expectedType}.`, data: { expectedType, actualType }, }); } @@ -219,7 +234,8 @@ export class INVALID_SCVAL_VEC_FOR_OPERATION extends OperationError { super({ code: Code.INVALID_SCVAL_VEC_FOR_OPERATION, message: `Invalid SCVal vector type for operation conversion.`, - details: `The SCVal type is null is not a vector type required for operation conversion.`, + details: + `The SCVal type is null is not a vector type required for operation conversion.`, data: {}, }); } @@ -230,7 +246,8 @@ export class INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION extends OperationError { super({ code: Code.INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION, message: `Invalid SCVal vector length for operation type ${opType}.`, - details: `The SCVal vector length ${actualLength} does not match the expected length ${expectedLength} for operation type ${opType}.`, + details: + `The SCVal vector length ${actualLength} does not match the expected length ${expectedLength} for operation type ${opType}.`, data: { opType, expectedLength, actualLength }, }); } @@ -240,8 +257,10 @@ export class INVALID_SCVAL_VEC_FOR_CONDITIONS extends OperationError { constructor(utxoOrPk: UTXOPublicKey | Ed25519PublicKey) { super({ code: Code.INVALID_SCVAL_VEC_FOR_CONDITIONS, - message: `Invalid SCVal vector for conditions in op with address ${utxoOrPk}.`, - details: `The SCVal vector for conditions in op with address ${utxoOrPk} is not valid.`, + message: + `Invalid SCVal vector for conditions in op with address ${utxoOrPk}.`, + details: + `The SCVal vector for conditions in op with address ${utxoOrPk} is not valid.`, data: { utxoOrPk }, }); } @@ -251,8 +270,10 @@ export class INVALID_SCVAL_VEC_FOR_CONDITION extends OperationError { constructor(utxoOrPk: UTXOPublicKey | Ed25519PublicKey, type: string) { super({ code: Code.INVALID_SCVAL_VEC_FOR_CONDITION, - message: `Invalid SCVal vector for condition in op with address ${utxoOrPk}.`, - details: `The SCVal vector for a condition in op with address ${utxoOrPk} is not valid. A type of ${type} was provided instead of an ScVec.`, + message: + `Invalid SCVal vector for condition in op with address ${utxoOrPk}.`, + details: + `The SCVal vector for a condition in op with address ${utxoOrPk} is not valid. A type of ${type} was provided instead of an ScVec.`, data: { utxoOrPk, type }, }); } diff --git a/src/operation/index.ts b/src/operation/index.ts index daaefff..fa5a335 100644 --- a/src/operation/index.ts +++ b/src/operation/index.ts @@ -81,13 +81,13 @@ export class MoonlightOperation implements BaseOperation { static deposit( publicKey: Ed25519PublicKey, - amount: bigint + amount: bigint, ): DepositOperation { assert(amount > 0n, new E.AMOUNT_TOO_LOW(amount)); assert( StrKey.isValidEd25519PublicKey(publicKey), - new E.INVALID_ED25519_PK(publicKey) + new E.INVALID_ED25519_PK(publicKey), ); return new MoonlightOperation({ @@ -99,13 +99,13 @@ export class MoonlightOperation implements BaseOperation { static withdraw( publicKey: Ed25519PublicKey, - amount: bigint + amount: bigint, ): WithdrawOperation { assert(amount > 0n, new E.AMOUNT_TOO_LOW(amount)); assert( StrKey.isValidEd25519PublicKey(publicKey), - new E.INVALID_ED25519_PK(publicKey) + new E.INVALID_ED25519_PK(publicKey), ); return new MoonlightOperation({ @@ -124,7 +124,7 @@ export class MoonlightOperation implements BaseOperation { static fromXDR( xdrString: string, - type: UTXOOperationType + type: UTXOOperationType, ): CreateOperation | DepositOperation | WithdrawOperation | SpendOperation { const scVal = xdr.ScVal.fromXDR(xdrString, "base64"); @@ -146,30 +146,30 @@ export class MoonlightOperation implements BaseOperation { static fromScVal( scVal: xdr.ScVal, - type: UTXOOperationType.CREATE + type: UTXOOperationType.CREATE, ): CreateOperation; static fromScVal( scVal: xdr.ScVal, - type: UTXOOperationType.DEPOSIT + type: UTXOOperationType.DEPOSIT, ): DepositOperation; static fromScVal( scVal: xdr.ScVal, - type: UTXOOperationType.WITHDRAW + type: UTXOOperationType.WITHDRAW, ): WithdrawOperation; static fromScVal( scVal: xdr.ScVal, - type: UTXOOperationType.SPEND + type: UTXOOperationType.SPEND, ): SpendOperation; public static fromScVal( scVal: xdr.ScVal, - type: UTXOOperationType + type: UTXOOperationType, ): CreateOperation | DepositOperation | WithdrawOperation | SpendOperation { assert( scVal.switch().name === xdr.ScValType.scvVec().name, new E.INVALID_SCVAL_TYPE_FOR_OPERATION( xdr.ScValType.scvVec().name, - scVal.switch().name - ) + scVal.switch().name, + ), ); const vec = scVal.vec(); @@ -179,7 +179,7 @@ export class MoonlightOperation implements BaseOperation { if (type === UTXOOperationType.CREATE) { assert( vec.length === 2, - new E.INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION(type, 2, vec.length) + new E.INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION(type, 2, vec.length), ); const utxo: UTXOPublicKey = Uint8Array.from(vec[0].bytes()); const amount = scValToBigInt(vec[1]); @@ -188,20 +188,20 @@ export class MoonlightOperation implements BaseOperation { if (type === UTXOOperationType.SPEND) { assert( vec.length === 2, - new E.INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION(type, 2, vec.length) + new E.INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION(type, 2, vec.length), ); const utxo: UTXOPublicKey = Uint8Array.from(vec[0].bytes()); const conditionsScVal = vec[1].vec(); assert( conditionsScVal !== null, - new E.INVALID_SCVAL_VEC_FOR_CONDITIONS(utxo) + new E.INVALID_SCVAL_VEC_FOR_CONDITIONS(utxo), ); const conditions: ConditionType[] = conditionsScVal.map((cScVal) => { assert( cScVal.switch().name === xdr.ScValType.scvVec().name, - new E.INVALID_SCVAL_VEC_FOR_CONDITION(utxo, cScVal.switch().name) + new E.INVALID_SCVAL_VEC_FOR_CONDITION(utxo, cScVal.switch().name), ); return Condition.fromScVal(cScVal); @@ -211,7 +211,7 @@ export class MoonlightOperation implements BaseOperation { if (type === UTXOOperationType.DEPOSIT) { assert( vec.length === 3, - new E.INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION(type, 3, vec.length) + new E.INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION(type, 3, vec.length), ); const publicKey = scValToNative(vec[0]) as Ed25519PublicKey; const amount = scValToBigInt(vec[1]); @@ -219,13 +219,16 @@ export class MoonlightOperation implements BaseOperation { assert( conditionsScVal !== null, - new E.INVALID_SCVAL_VEC_FOR_CONDITIONS(publicKey) + new E.INVALID_SCVAL_VEC_FOR_CONDITIONS(publicKey), ); const conditions: ConditionType[] = conditionsScVal.map((cScVal) => { assert( cScVal.switch().name === xdr.ScValType.scvVec().name, - new E.INVALID_SCVAL_VEC_FOR_CONDITION(publicKey, cScVal.switch().name) + new E.INVALID_SCVAL_VEC_FOR_CONDITION( + publicKey, + cScVal.switch().name, + ), ); return Condition.fromScVal(cScVal); }); @@ -234,7 +237,7 @@ export class MoonlightOperation implements BaseOperation { if (type === UTXOOperationType.WITHDRAW) { assert( vec.length === 3, - new E.INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION(type, 3, vec.length) + new E.INVALID_SCVAL_VEC_LENGTH_FOR_OPERATION(type, 3, vec.length), ); const publicKey = scValToNative(vec[0]) as Ed25519PublicKey; const amount = scValToBigInt(vec[1]); @@ -242,13 +245,16 @@ export class MoonlightOperation implements BaseOperation { assert( conditionsScVal !== null, - new E.INVALID_SCVAL_VEC_FOR_CONDITIONS(publicKey) + new E.INVALID_SCVAL_VEC_FOR_CONDITIONS(publicKey), ); const conditions: ConditionType[] = conditionsScVal.map((cScVal) => { assert( cScVal.switch().name === xdr.ScValType.scvVec().name, - new E.INVALID_SCVAL_VEC_FOR_CONDITION(publicKey, cScVal.switch().name) + new E.INVALID_SCVAL_VEC_FOR_CONDITION( + publicKey, + cScVal.switch().name, + ), ); return Condition.fromScVal(cScVal); }); @@ -287,7 +293,7 @@ export class MoonlightOperation implements BaseOperation { | "_utxo" | "_conditions" | "_utxoSignature" - | "_ed25519Signature" + | "_ed25519Signature", ): | UTXOOperationType | bigint @@ -437,11 +443,11 @@ export class MoonlightOperation implements BaseOperation { } public appendEd25519Signature( - signature: xdr.SorobanAuthorizationEntry + signature: xdr.SorobanAuthorizationEntry, ): this { assert( this.isSignedByEd25519() === false, - new E.OP_ALREADY_SIGNED("Ed25519") + new E.OP_ALREADY_SIGNED("Ed25519"), ); this.setEd25519Signature(signature); return this; @@ -480,13 +486,13 @@ export class MoonlightOperation implements BaseOperation { public async signWithUTXO( utxo: IUTXOKeypairBase, channelId: ContractId, - signatureExpirationLedger: number + signatureExpirationLedger: number, ): Promise { assert(this.isSignedByUTXO() === false, new E.OP_ALREADY_SIGNED("UTXO")); assert( this.getOperation() === UTXOOperationType.SPEND, - new E.OP_IS_NOT_SIGNABLE(this.getOperation(), "UTXO") + new E.OP_IS_NOT_SIGNABLE(this.getOperation(), "UTXO"), ); const conditions = this.getConditions(); @@ -498,7 +504,7 @@ export class MoonlightOperation implements BaseOperation { contractId: channelId, conditions, liveUntilLedger: signatureExpirationLedger, - }) + }), ); this.appendUTXOSignature({ @@ -515,24 +521,24 @@ export class MoonlightOperation implements BaseOperation { channelId: ContractId, assetId: ContractId, networkPassphrase: string, - nonce?: string + nonce?: string, ): Promise { assert( this.isSignedByEd25519() === false, - new E.OP_ALREADY_SIGNED("Ed25519") + new E.OP_ALREADY_SIGNED("Ed25519"), ); assert( this.getOperation() === UTXOOperationType.DEPOSIT, - new E.OP_IS_NOT_SIGNABLE(this.getOperation(), "Ed25519") + new E.OP_IS_NOT_SIGNABLE(this.getOperation(), "Ed25519"), ); assert( depositorKeys.publicKey() === this.getPublicKey(), new E.SIGNER_IS_NOT_DEPOSITOR( depositorKeys.publicKey(), - this.getPublicKey() - ) + this.getPublicKey(), + ), ); if (!nonce) nonce = generateNonce(); @@ -555,14 +561,14 @@ export class MoonlightOperation implements BaseOperation { signedAuthEntry = await depositorKeys.signSorobanAuthEntry( rawAuthEntry, signatureExpirationLedger, - networkPassphrase + networkPassphrase, ); } else { signedAuthEntry = await authorizeEntry( rawAuthEntry, depositorKeys, signatureExpirationLedger, - networkPassphrase + networkPassphrase, ); } @@ -652,8 +658,8 @@ export class MoonlightOperation implements BaseOperation { public hasConditions(): boolean { return this._conditions !== undefined && - "length" in this._conditions && - this._conditions.length > 0 + "length" in this._conditions && + this._conditions.length > 0 ? true : false; } @@ -791,12 +797,12 @@ export class MoonlightOperation implements BaseOperation { | CreateOperation | DepositOperation | WithdrawOperation - | SpendOperation + | SpendOperation, ); } static fromMLXDR( - mlxdrString: string + mlxdrString: string, ): CreateOperation | DepositOperation | WithdrawOperation | SpendOperation { return MLXDR.toOperation(mlxdrString); } diff --git a/src/operation/index.unit.test.ts b/src/operation/index.unit.test.ts index 80fd970..7cf5f9a 100644 --- a/src/operation/index.unit.test.ts +++ b/src/operation/index.unit.test.ts @@ -24,8 +24,8 @@ describe("Condition", () => { let network: string; beforeAll(async () => { - validPublicKey = - LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + validPublicKey = LocalSigner.generateRandom() + .publicKey() as Ed25519PublicKey; validUtxo = (await generateP256KeyPair()).publicKey as UTXOPublicKey; channelId = @@ -42,7 +42,7 @@ describe("Condition", () => { const scVal = createOp.toScVal(); const recreatedOp = MoonlightOperation.fromScVal( scVal, - UTXOOperationType.CREATE + UTXOOperationType.CREATE, ); assertEquals(recreatedOp.getOperation(), UTXOOperationType.CREATE); @@ -67,7 +67,7 @@ describe("Condition", () => { const scVal = spendOp.toScVal(); const recreatedOp = MoonlightOperation.fromScVal( scVal, - UTXOOperationType.SPEND + UTXOOperationType.SPEND, ); assertEquals(recreatedOp.getOperation(), UTXOOperationType.SPEND); @@ -90,15 +90,15 @@ describe("Condition", () => { it("should convert to and from ScVal correctly for Spend operation with conditions", () => { const spendOp = MoonlightOperation.spend(validUtxo); spendOp.addCondition( - MoonlightOperation.create(validUtxo, 500n).toCondition() + MoonlightOperation.create(validUtxo, 500n).toCondition(), ); spendOp.addCondition( - MoonlightOperation.create(validUtxo, 300n).toCondition() + MoonlightOperation.create(validUtxo, 300n).toCondition(), ); const scVal = spendOp.toScVal(); const recreatedOp = MoonlightOperation.fromScVal( scVal, - UTXOOperationType.SPEND + UTXOOperationType.SPEND, ); assertEquals(recreatedOp.getOperation(), UTXOOperationType.SPEND); @@ -107,13 +107,13 @@ describe("Condition", () => { assertEquals(recreatedOp.getConditions()[0].isCreate(), true); assertEquals( (recreatedOp.getConditions()[0] as CreateCondition).getUtxo(), - validUtxo + validUtxo, ); assertEquals(recreatedOp.getConditions()[0].getAmount(), 500n); assertEquals(recreatedOp.getConditions()[1].isCreate(), true); assertEquals( (recreatedOp.getConditions()[1] as CreateCondition).getUtxo(), - validUtxo + validUtxo, ); assertEquals(recreatedOp.getConditions()[1].getAmount(), 300n); @@ -133,14 +133,14 @@ describe("Condition", () => { const scVal = depositOp.toScVal(); const recreatedOp = MoonlightOperation.fromScVal( scVal, - UTXOOperationType.DEPOSIT + UTXOOperationType.DEPOSIT, ); assertEquals(recreatedOp.getOperation(), UTXOOperationType.DEPOSIT); assertEquals(recreatedOp.getAmount(), 20n); assertEquals( recreatedOp.getPublicKey().toString(), - validPublicKey.toString() + validPublicKey.toString(), ); assertEquals(recreatedOp.getConditions().length, 0); @@ -155,37 +155,37 @@ describe("Condition", () => { // Cannot enforce against withdraw as it shares the same ScVal structure as Deposit depositOp.addCondition( - MoonlightOperation.create(validUtxo, 200n).toCondition() + MoonlightOperation.create(validUtxo, 200n).toCondition(), ); const scValWithCondition = depositOp.toScVal(); const recreatedOpWithCondition = MoonlightOperation.fromScVal( scValWithCondition, - UTXOOperationType.DEPOSIT + UTXOOperationType.DEPOSIT, ); assertEquals( recreatedOpWithCondition.getOperation(), - UTXOOperationType.DEPOSIT + UTXOOperationType.DEPOSIT, ); assertEquals(recreatedOpWithCondition.getAmount(), 20n); assertEquals( recreatedOpWithCondition.getPublicKey().toString(), - validPublicKey.toString() + validPublicKey.toString(), ); assertEquals(recreatedOpWithCondition.getConditions().length, 1); assertEquals( recreatedOpWithCondition.getConditions()[0].isCreate(), - true + true, ); assertEquals( ( recreatedOpWithCondition.getConditions()[0] as CreateCondition ).getUtxo(), - validUtxo + validUtxo, ); assertEquals( recreatedOpWithCondition.getConditions()[0].getAmount(), - 200n + 200n, ); }); @@ -194,14 +194,14 @@ describe("Condition", () => { const scVal = withdrawOp.toScVal(); const recreatedOp = MoonlightOperation.fromScVal( scVal, - UTXOOperationType.WITHDRAW + UTXOOperationType.WITHDRAW, ); assertEquals(recreatedOp.getOperation(), UTXOOperationType.WITHDRAW); assertEquals(recreatedOp.getAmount(), 30n); assertEquals( recreatedOp.getPublicKey().toString(), - validPublicKey.toString() + validPublicKey.toString(), ); assertEquals(recreatedOp.getConditions().length, 0); @@ -216,37 +216,37 @@ describe("Condition", () => { // Cannot enforce against deposit as it shares the same ScVal structure as Withdraw withdrawOp.addCondition( - MoonlightOperation.create(validUtxo, 300n).toCondition() + MoonlightOperation.create(validUtxo, 300n).toCondition(), ); const scValWithCondition = withdrawOp.toScVal(); const recreatedOpWithCondition = MoonlightOperation.fromScVal( scValWithCondition, - UTXOOperationType.WITHDRAW + UTXOOperationType.WITHDRAW, ); assertEquals( recreatedOpWithCondition.getOperation(), - UTXOOperationType.WITHDRAW + UTXOOperationType.WITHDRAW, ); assertEquals(recreatedOpWithCondition.getAmount(), 30n); assertEquals( recreatedOpWithCondition.getPublicKey().toString(), - validPublicKey.toString() + validPublicKey.toString(), ); assertEquals(recreatedOpWithCondition.getConditions().length, 1); assertEquals( recreatedOpWithCondition.getConditions()[0].isCreate(), - true + true, ); assertEquals( ( recreatedOpWithCondition.getConditions()[0] as CreateCondition ).getUtxo(), - validUtxo + validUtxo, ); assertEquals( recreatedOpWithCondition.getConditions()[0].getAmount(), - 300n + 300n, ); }); }); @@ -257,7 +257,7 @@ describe("Condition", () => { const mlxdr = createOp.toMLXDR(); const recreatedOp = MoonlightOperation.fromMLXDR( - mlxdr + mlxdr, ) as MoonlightOperation; assertEquals(recreatedOp.getOperation(), UTXOOperationType.CREATE); assertEquals(recreatedOp.getAmount(), 10n); @@ -268,14 +268,14 @@ describe("Condition", () => { const depositOp = MoonlightOperation.deposit(validPublicKey, 20n); const mlxdr = depositOp.toMLXDR(); const recreatedOp = MoonlightOperation.fromMLXDR( - mlxdr + mlxdr, ) as MoonlightOperation; assertEquals(recreatedOp.getOperation(), UTXOOperationType.DEPOSIT); assertEquals(recreatedOp.getAmount(), 20n); assertEquals( recreatedOp.getPublicKey().toString(), - validPublicKey.toString() + validPublicKey.toString(), ); }); @@ -283,14 +283,14 @@ describe("Condition", () => { const withdrawOp = MoonlightOperation.withdraw(validPublicKey, 30n); const mlxdr = withdrawOp.toMLXDR(); const recreatedOp = MoonlightOperation.fromMLXDR( - mlxdr + mlxdr, ) as MoonlightOperation; assertEquals(recreatedOp.getOperation(), UTXOOperationType.WITHDRAW); assertEquals(recreatedOp.getAmount(), 30n); assertEquals( recreatedOp.getPublicKey().toString(), - validPublicKey.toString() + validPublicKey.toString(), ); }); @@ -298,7 +298,7 @@ describe("Condition", () => { const spendOp = MoonlightOperation.spend(validUtxo); const mlxdr = spendOp.toMLXDR(); const recreatedOp = MoonlightOperation.fromMLXDR( - mlxdr + mlxdr, ) as MoonlightOperation; assertEquals(recreatedOp.getOperation(), UTXOOperationType.SPEND); @@ -309,7 +309,7 @@ describe("Condition", () => { const ed25519Signer = LocalSigner.generateRandom(); const depositOp = MoonlightOperation.deposit( ed25519Signer.publicKey() as Ed25519PublicKey, - 50n + 50n, ); depositOp.addCondition(Condition.create(validUtxo, 400n)); @@ -319,26 +319,26 @@ describe("Condition", () => { 100, channelId, assetId, - network + network, ); const mlxdr = depositOp.toMLXDR(); const recreatedOp = MoonlightOperation.fromMLXDR( - mlxdr + mlxdr, ) as MoonlightOperation; assertEquals(recreatedOp.getOperation(), UTXOOperationType.DEPOSIT); assertEquals(recreatedOp.getAmount(), 50n); assertEquals( recreatedOp.getPublicKey().toString(), - ed25519Signer.publicKey().toString() + ed25519Signer.publicKey().toString(), ); assertEquals(recreatedOp.getConditions().length, 1); assertExists(recreatedOp.getEd25519Signature()); assertEquals( recreatedOp.getEd25519Signature().toXDR(), - depositOp.getEd25519Signature().toXDR() + depositOp.getEd25519Signature().toXDR(), ); const utxoSigner = new UTXOKeypairBase(await generateP256KeyPair()); @@ -350,23 +350,23 @@ describe("Condition", () => { const spendMlxdr = spendOp.toMLXDR(); const recreatedSpendOp = MoonlightOperation.fromMLXDR( - spendMlxdr + spendMlxdr, ) as MoonlightOperation; assertEquals(recreatedSpendOp.getOperation(), UTXOOperationType.SPEND); assertEquals( recreatedSpendOp.getUtxo().toString(), - utxoSigner.publicKey.toString() + utxoSigner.publicKey.toString(), ); assertEquals(recreatedSpendOp.getConditions().length, 1); assertExists(recreatedSpendOp.getUTXOSignature()); assertEquals( recreatedSpendOp.getUTXOSignature().sig, - spendOp.getUTXOSignature().sig + spendOp.getUTXOSignature().sig, ); assertEquals( recreatedSpendOp.getUTXOSignature().exp, - spendOp.getUTXOSignature().exp + spendOp.getUTXOSignature().exp, ); }); }); diff --git a/src/operation/types.ts b/src/operation/types.ts index 2bf3fa6..e4808ac 100644 --- a/src/operation/types.ts +++ b/src/operation/types.ts @@ -57,7 +57,7 @@ export interface DepositOperation extends BaseOperation { channelId: ContractId, assetId: ContractId, networkPassphrase: string, - nonce?: string + nonce?: string, ): Promise; getEd25519Signature(): xdr.SorobanAuthorizationEntry; isSignedByEd25519(): boolean; @@ -78,7 +78,7 @@ export interface SpendOperation extends BaseOperation { signWithUTXO( utxo: IUTXOKeypairBase, channelId: ContractId, - signatureExpirationLedger: number + signatureExpirationLedger: number, ): Promise; appendUTXOSignature(signature: OperationSignature): this; } diff --git a/src/privacy-channel/error.ts b/src/privacy-channel/error.ts index c540ac0..64d6a93 100644 --- a/src/privacy-channel/error.ts +++ b/src/privacy-channel/error.ts @@ -45,7 +45,8 @@ export class PROPERTY_NOT_SET extends PrivacyChannelError { super({ code: Code.PROPERTY_NOT_SET, message: `Property not set: ${property}`, - details: `The required property ${property} is not set in the privacy channel.`, + details: + `The required property ${property} is not set in the privacy channel.`, data: { property }, }); } diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index d10b8df..61a96eb 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -1,7 +1,7 @@ import { Contract, - type NetworkConfig, type ContractId, + type NetworkConfig, type TransactionConfig, } from "@colibri/core"; import { StellarDerivator } from "../derivation/stellar/index.ts"; @@ -26,7 +26,7 @@ export class PrivacyChannel { networkConfig: NetworkConfig, channelId: ContractId, authId: ContractId, - assetId: ContractId + assetId: ContractId, ) { this._networkConfig = networkConfig; @@ -41,7 +41,7 @@ export class PrivacyChannel { this._derivator = new StellarDerivator().withNetworkAndContract( networkConfig.networkPassphrase as StellarNetworkId, - channelId as ContractId + channelId as ContractId, ); } @@ -57,14 +57,14 @@ export class PrivacyChannel { * @param arg - The name of the property to retrieve. * @returns The value of the requested property. * @throws {Error} If the requested property is not set. - * */ + */ private require(arg: "_client"): Contract; private require(arg: "_authId"): ContractId; private require(arg: "_networkConfig"): NetworkConfig; private require(arg: "_derivator"): StellarDerivator; private require(arg: "_assetId"): ContractId; private require( - arg: "_client" | "_authId" | "_networkConfig" | "_derivator" | "_assetId" + arg: "_client" | "_authId" | "_networkConfig" | "_derivator" | "_assetId", ): Contract | ContractId | NetworkConfig | StellarDerivator { if (this[arg]) return this[arg]; throw new E.PROPERTY_NOT_SET(arg); @@ -82,7 +82,7 @@ export class PrivacyChannel { * @params None * @returns {Contract} The Contract client instance. * @throws {Error} If the client instance is not set. - * */ + */ private getClient(): Contract { return this.require("_client"); } @@ -93,7 +93,7 @@ export class PrivacyChannel { * @params None * @returns {ContractId} The Auth contract ID. * @throws {Error} If the Auth contract ID is not set. - * */ + */ public getAuthId(): ContractId { return this.require("_authId"); } @@ -104,7 +104,7 @@ export class PrivacyChannel { * @params None * @returns {ContractId} The Asset contract ID. * @throws {Error} If the Asset contract ID is not set. - * */ + */ public getAssetId(): ContractId { return this.require("_assetId"); } @@ -115,7 +115,7 @@ export class PrivacyChannel { * @params None * @returns {NetworkConfig} The NetworkConfig instance. * @throws {Error} If the NetworkConfig instance is not set. - * */ + */ public getNetworkConfig(): NetworkConfig { return this.require("_networkConfig"); } @@ -126,7 +126,7 @@ export class PrivacyChannel { * @params None * @returns {StellarDerivator} The StellarDerivator instance. * @throws {Error} If the StellarDerivator instance is not set. - * */ + */ public getDerivator(): StellarDerivator { return this.require("_derivator"); } @@ -137,7 +137,7 @@ export class PrivacyChannel { * @params None * @returns {ContractId} The Contract ID of the privacy channel contract. * @throws {Error} If the client instance is not set. - * */ + */ public getChannelId(): ContractId { return this.getClient().getContractId(); } @@ -155,7 +155,7 @@ export class PrivacyChannel { * @param {M} args.method - The read method to call. * @param {ChannelReadMethods[M]["input"]} args.methodArgs - The arguments for the read method. * @returns {Promise} A promise that resolves to the output of the read method. - * */ + */ public async read(args: { method: M; @@ -173,7 +173,7 @@ export class PrivacyChannel { * @param {M} args.method - The write method to call. * @param {ChannelInvokeMethods[M]["input"]} args.methodArgs - The arguments for the write method. * @returns {ReturnType} A promise that resolves to the invoke colibri response. - * */ + */ public async invoke(args: { method: M; methodArgs: ChannelInvoke[M]["input"]; @@ -193,7 +193,7 @@ export class PrivacyChannel { * @param { xdr.SorobanAuthorizationEntry[] } [args.operationArgs.auth] - Optional authorization entries. * @param { TransactionConfig } args.config - The transaction configuration. * @returns {ReturnType} A promise that resolves to the invoke colibri response. - * */ + */ public async invokeRaw(args: { operationArgs: { function: string; diff --git a/src/transaction-builder/auth/payload-hash.ts b/src/transaction-builder/auth/payload-hash.ts index 6085afa..c01563f 100644 --- a/src/transaction-builder/auth/payload-hash.ts +++ b/src/transaction-builder/auth/payload-hash.ts @@ -1,4 +1,4 @@ -import { xdr, hash } from "@stellar/stellar-sdk"; +import { hash, xdr } from "@stellar/stellar-sdk"; import { Buffer } from "buffer"; import { sha256Buffer } from "../../utils/hash/sha256Buffer.ts"; @@ -24,5 +24,3 @@ export const buildOperationAuthEntryHash = async (params: { const payload = preImage.toXDR(); return Buffer.from(await sha256Buffer(payload)); }; - - diff --git a/src/transaction-builder/error.ts b/src/transaction-builder/error.ts index 519745b..3520bbc 100644 --- a/src/transaction-builder/error.ts +++ b/src/transaction-builder/error.ts @@ -15,8 +15,6 @@ export type TransactionBuilderErrorShape = { data: unknown; }; - - export enum Code { UNEXPECTED_ERROR = "TBU_000", PROPERTY_NOT_SET = "TBU_001", @@ -64,7 +62,8 @@ export class PROPERTY_NOT_SET extends TransactionBuilderError { super({ code: Code.PROPERTY_NOT_SET, message: `Property not set: ${property}`, - details: `The required property ${property} is not set in the transaction builder.`, + details: + `The required property ${property} is not set in the transaction builder.`, data: { property }, }); } @@ -75,7 +74,8 @@ export class UNSUPPORTED_OP_TYPE extends TransactionBuilderError { super({ code: Code.UNSUPPORTED_OP_TYPE, message: `Unsupported operation type: ${opType}`, - details: `The operation type ${opType} is not supported in the transaction builder.`, + details: + `The operation type ${opType} is not supported in the transaction builder.`, data: { opType }, }); } @@ -86,7 +86,8 @@ export class DUPLICATE_CREATE_OP extends TransactionBuilderError { super({ code: Code.DUPLICATE_CREATE_OP, message: `Duplicate create operation for UTXO public key: ${utxoPk}`, - details: `A create operation for the UTXO public key ${utxoPk} already exists in the transaction builder.`, + details: + `A create operation for the UTXO public key ${utxoPk} already exists in the transaction builder.`, data: { utxoPk }, }); } @@ -97,7 +98,8 @@ export class DUPLICATE_SPEND_OP extends TransactionBuilderError { super({ code: Code.DUPLICATE_SPEND_OP, message: `Duplicate spend operation for UTXO public key: ${utxoPk}`, - details: `A spend operation for the UTXO public key ${utxoPk} already exists in the transaction builder.`, + details: + `A spend operation for the UTXO public key ${utxoPk} already exists in the transaction builder.`, data: { utxoPk }, }); } @@ -108,7 +110,8 @@ export class DUPLICATE_DEPOSIT_OP extends TransactionBuilderError { super({ code: Code.DUPLICATE_DEPOSIT_OP, message: `Duplicate deposit operation for public key: ${publicKey}`, - details: `A deposit operation for the public key ${publicKey} already exists in the transaction builder.`, + details: + `A deposit operation for the public key ${publicKey} already exists in the transaction builder.`, data: { publicKey }, }); } @@ -118,7 +121,8 @@ export class DUPLICATE_WITHDRAW_OP extends TransactionBuilderError { super({ code: Code.DUPLICATE_WITHDRAW_OP, message: `Duplicate withdraw operation for public key: ${publicKey}`, - details: `A withdraw operation for the public key ${publicKey} already exists in the transaction builder.`, + details: + `A withdraw operation for the public key ${publicKey} already exists in the transaction builder.`, data: { publicKey }, }); } @@ -129,7 +133,8 @@ export class NO_SPEND_OPS extends TransactionBuilderError { super({ code: Code.NO_SPEND_OPS, message: `No spend operations found for the UTXO: ${utxoPk}`, - details: `There are no spend operations associated with the UTXO public key ${utxoPk} in the transaction builder.`, + details: + `There are no spend operations associated with the UTXO public key ${utxoPk} in the transaction builder.`, data: { utxoPk }, }); } @@ -140,7 +145,8 @@ export class NO_DEPOSIT_OPS extends TransactionBuilderError { super({ code: Code.NO_DEPOSIT_OPS, message: `No deposit operations found for the public key: ${publicKey}`, - details: `There are no deposit operations associated with the public key ${publicKey} in the transaction builder.`, + details: + `There are no deposit operations associated with the public key ${publicKey} in the transaction builder.`, data: { publicKey }, }); } @@ -151,7 +157,8 @@ export class NO_WITHDRAW_OPS extends TransactionBuilderError { super({ code: Code.NO_WITHDRAW_OPS, message: `No withdraw operations found for the public key: ${publicKey}`, - details: `There are no withdraw operations associated with the public key ${publicKey} in the transaction builder.`, + details: + `There are no withdraw operations associated with the public key ${publicKey} in the transaction builder.`, data: { publicKey }, }); } @@ -161,8 +168,10 @@ export class NO_EXT_OPS extends TransactionBuilderError { constructor(publicKey: Ed25519PublicKey) { super({ code: Code.NO_EXT_OPS, - message: `No deposit or withdraw operations found for the public key: ${publicKey}`, - details: `There are no deposit or withdraw operations associated with the public key ${publicKey} in the transaction builder.`, + message: + `No deposit or withdraw operations found for the public key: ${publicKey}`, + details: + `There are no deposit or withdraw operations associated with the public key ${publicKey} in the transaction builder.`, data: { publicKey }, }); } @@ -173,7 +182,8 @@ export class AMOUNT_TOO_LOW extends TransactionBuilderError { super({ code: Code.AMOUNT_TOO_LOW, message: `Amount too low: ${amount}`, - details: `The provided amount ${amount} is below the minimum required. It must be greater than zero.`, + details: + `The provided amount ${amount} is below the minimum required. It must be greater than zero.`, data: { amount: `${amount}` }, }); } @@ -184,7 +194,8 @@ export class MISSING_PROVIDER_SIGNATURE extends TransactionBuilderError { super({ code: Code.MISSING_PROVIDER_SIGNATURE, message: `Missing provider signature`, - details: `No provider signatures have been added to the transaction builder.`, + details: + `No provider signatures have been added to the transaction builder.`, data: {}, }); } @@ -195,7 +206,8 @@ export class NO_CONDITIONS_FOR_SPEND_OP extends TransactionBuilderError { super({ code: Code.NO_CONDITIONS_FOR_SPEND_OP, message: `No conditions found for spend operation with UTXO: ${utxoPk}`, - details: `The spend operation associated with the UTXO public key ${utxoPk} does not have any conditions set in the transaction builder.`, + details: + `The spend operation associated with the UTXO public key ${utxoPk} does not have any conditions set in the transaction builder.`, data: { utxoPk }, }); } diff --git a/src/transaction-builder/index.ts b/src/transaction-builder/index.ts index 1ffd04c..28d4147 100644 --- a/src/transaction-builder/index.ts +++ b/src/transaction-builder/index.ts @@ -16,25 +16,25 @@ import { import { orderSpendByUtxo } from "./utils/index.ts"; import { assertNoDuplicateCreate, - assertNoDuplicateSpend, assertNoDuplicateDeposit, + assertNoDuplicateSpend, assertNoDuplicateWithdraw, assertSpendExists, } from "./validators/index.ts"; import { type ContractId, type Ed25519PublicKey, - type TransactionSigner, isTransactionSigner, + type TransactionSigner, } from "@colibri/core"; import type { + BaseOperation, CreateOperation, DepositOperation, - SpendOperation, - WithdrawOperation, MoonlightOperation, - BaseOperation, OperationSignature, + SpendOperation, + WithdrawOperation, } from "../operation/types.ts"; import * as E from "./error.ts"; import { assert } from "../utils/assert/assert.ts"; @@ -97,13 +97,13 @@ export class MoonlightTransactionBuilder { private require(arg: "_assetId"): ContractId; private require(arg: "_network"): string; private require( - arg: "_innerSignatures" + arg: "_innerSignatures", ): Map; private require( - arg: "_providerInnerSignatures" + arg: "_providerInnerSignatures", ): Map; private require( - arg: "_extSignatures" + arg: "_extSignatures", ): Map; private require( arg: @@ -117,7 +117,7 @@ export class MoonlightTransactionBuilder { | "_network" | "_innerSignatures" | "_providerInnerSignatures" - | "_extSignatures" + | "_extSignatures", ): | CreateOperation[] | SpendOperation[] @@ -347,7 +347,7 @@ export class MoonlightTransactionBuilder { private addInnerSignature( utxo: UTXOPublicKey, - signature: OperationSignature + signature: OperationSignature, ): MoonlightTransactionBuilder { assertSpendExists(this.getSpendOperations(), utxo); @@ -359,7 +359,7 @@ export class MoonlightTransactionBuilder { pubKey: Ed25519PublicKey, signature: Buffer, expirationLedger: number, - nonce: string + nonce: string, ): MoonlightTransactionBuilder { this.providerInnerSignatures.set(pubKey, { sig: signature, @@ -371,12 +371,12 @@ export class MoonlightTransactionBuilder { public addExtSignedEntry( pubKey: Ed25519PublicKey, - signedAuthEntry: xdr.SorobanAuthorizationEntry + signedAuthEntry: xdr.SorobanAuthorizationEntry, ): MoonlightTransactionBuilder { assertExtOpsExist( this.getDepositOperations(), this.getWithdrawOperations(), - pubKey + pubKey, ); this.extSignatures.set(pubKey, signedAuthEntry); @@ -384,10 +384,10 @@ export class MoonlightTransactionBuilder { } public getDepositOperation( - depositor: Ed25519PublicKey + depositor: Ed25519PublicKey, ): DepositOperation | undefined { return this.getDepositOperations().find( - (d) => d.getPublicKey() === depositor + (d) => d.getPublicKey() === depositor, ); } @@ -412,7 +412,7 @@ export class MoonlightTransactionBuilder { xdr.ScVal.scvBytes(Buffer.from(spend.getUtxo() as Uint8Array)), ]), val: xdr.ScVal.scvVec(spend.getConditions().map((c) => c.toScVal())), - }) + }), ); } @@ -422,7 +422,7 @@ export class MoonlightTransactionBuilder { public getOperationAuthEntry( nonce: string, signatureExpirationLedger: number, - signed: boolean = false + signed: boolean = false, ): xdr.SorobanAuthorizationEntry { const reqArgs: xdr.ScVal[] = this.getAuthRequirementArgs(); @@ -439,11 +439,12 @@ export class MoonlightTransactionBuilder { public getSignedOperationAuthEntry(): xdr.SorobanAuthorizationEntry { const providerSigners = Array.from(this.providerInnerSignatures.keys()); - if (providerSigners.length === 0) + if (providerSigners.length === 0) { throw new Error("No Provider signatures added"); + } - const { nonce, exp: signatureExpirationLedger } = - this.providerInnerSignatures.get(providerSigners[0])!; + const { nonce, exp: signatureExpirationLedger } = this + .providerInnerSignatures.get(providerSigners[0])!; const reqArgs: xdr.ScVal[] = this.getAuthRequirementArgs(); @@ -459,11 +460,11 @@ export class MoonlightTransactionBuilder { public async getOperationAuthEntryHash( nonce: string, - signatureExpirationLedger: number + signatureExpirationLedger: number, ): Promise { const rootInvocation = this.getOperationAuthEntry( nonce, - signatureExpirationLedger + signatureExpirationLedger, ).rootInvocation(); return await buildOperationAuthEntryHash({ network: this.network, @@ -479,7 +480,7 @@ export class MoonlightTransactionBuilder { assert(providerSigners.length > 0, new E.MISSING_PROVIDER_SIGNATURE()); const spendSigs = Array.from(this.innerSignatures.entries()).map( - ([utxo, { sig, exp }]) => ({ utxo, sig, exp }) + ([utxo, { sig, exp }]) => ({ utxo, sig, exp }), ); const providerSigs = providerSigners.map((pk) => { const { sig, exp } = this.providerInnerSignatures.get(pk)!; @@ -492,31 +493,31 @@ export class MoonlightTransactionBuilder { public async signWithProvider( providerKeys: TransactionSigner | Keypair, signatureExpirationLedger: number, - nonce?: string + nonce?: string, ) { if (!nonce) nonce = generateNonce(); const authHash = await this.getOperationAuthEntryHash( nonce, - signatureExpirationLedger + signatureExpirationLedger, ); const signedHash = isTransactionSigner(providerKeys) - ? // deno-lint-ignore no-explicit-any - providerKeys.sign(authHash as any) + // deno-lint-ignore no-explicit-any + ? providerKeys.sign(authHash as any) : providerKeys.sign(authHash); this.addProviderInnerSignature( providerKeys.publicKey() as Ed25519PublicKey, signedHash as Buffer, signatureExpirationLedger, - nonce + nonce, ); } public async signWithSpendUtxo( utxoKp: IUTXOKeypairBase, - signatureExpirationLedger: number + signatureExpirationLedger: number, ) { const spendOp = this.getSpendOperation(utxoKp.publicKey); @@ -525,7 +526,7 @@ export class MoonlightTransactionBuilder { await spendOp.signWithUTXO( utxoKp, this.getChannelId(), - signatureExpirationLedger + signatureExpirationLedger, ); this.addInnerSignature(utxoKp.publicKey, spendOp.getUTXOSignature()); @@ -534,15 +535,15 @@ export class MoonlightTransactionBuilder { public async signExtWithEd25519( keys: TransactionSigner | Keypair, signatureExpirationLedger: number, - nonce?: string + nonce?: string, ) { const depositOp = this.getDepositOperation( - keys.publicKey() as Ed25519PublicKey + keys.publicKey() as Ed25519PublicKey, ); assert( depositOp, - new E.NO_DEPOSIT_OPS(keys.publicKey() as Ed25519PublicKey) + new E.NO_DEPOSIT_OPS(keys.publicKey() as Ed25519PublicKey), ); await depositOp.signWithEd25519( @@ -551,12 +552,12 @@ export class MoonlightTransactionBuilder { this.getChannelId(), this.getAssetId(), this.network, - nonce + nonce, ); this.addExtSignedEntry( keys.publicKey() as Ed25519PublicKey, - depositOp.getEd25519Signature() + depositOp.getEd25519Signature(), ); } @@ -584,25 +585,25 @@ export class MoonlightTransactionBuilder { new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol("create"), val: xdr.ScVal.scvVec( - this.getCreateOperations().map((op) => op.toScVal()) + this.getCreateOperations().map((op) => op.toScVal()), ), }), new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol("deposit"), val: xdr.ScVal.scvVec( - this.getDepositOperations().map((op) => op.toScVal()) + this.getDepositOperations().map((op) => op.toScVal()), ), }), new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol("spend"), val: xdr.ScVal.scvVec( - this.getSpendOperations().map((op) => op.toScVal()) + this.getSpendOperations().map((op) => op.toScVal()), ), }), new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol("withdraw"), val: xdr.ScVal.scvVec( - this.getWithdrawOperations().map((op) => op.toScVal()) + this.getWithdrawOperations().map((op) => op.toScVal()), ), }), ]); diff --git a/src/transaction-builder/index.unit.test.ts b/src/transaction-builder/index.unit.test.ts index b109899..e9584af 100644 --- a/src/transaction-builder/index.unit.test.ts +++ b/src/transaction-builder/index.unit.test.ts @@ -7,7 +7,7 @@ import { import { beforeAll, describe, it } from "@std/testing/bdd"; import { LocalSigner } from "@colibri/core"; import { Asset, Networks } from "@stellar/stellar-sdk"; -import type { Ed25519PublicKey, ContractId } from "@colibri/core"; +import type { ContractId, Ed25519PublicKey } from "@colibri/core"; import { MoonlightTransactionBuilder } from "./index.ts"; import { Condition } from "../conditions/index.ts"; import { MoonlightOperation as Operation } from "../operation/index.ts"; @@ -31,8 +31,8 @@ describe("MoonlightTransactionBuilder", () => { let builder: MoonlightTransactionBuilder; beforeAll(() => { - validPublicKey = - LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + validPublicKey = LocalSigner.generateRandom() + .publicKey() as Ed25519PublicKey; validAmount = 1000n; channelId = @@ -102,8 +102,8 @@ describe("MoonlightTransactionBuilder", () => { }); it("should return correct deposit operation by public key", () => { - const pubKey = - LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + const pubKey = LocalSigner.generateRandom() + .publicKey() as Ed25519PublicKey; const condition = Condition.deposit(pubKey, validAmount); const operation = Operation.deposit(pubKey, validAmount); operation.addCondition(condition); @@ -125,8 +125,8 @@ describe("MoonlightTransactionBuilder", () => { }); it("should return undefined for non-existent deposit operation", () => { - const nonExistentKey = - LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + const nonExistentKey = LocalSigner.generateRandom() + .publicKey() as Ed25519PublicKey; const foundOperation = builder.getDepositOperation(nonExistentKey); assertEquals(foundOperation, undefined); }); @@ -168,7 +168,7 @@ describe("MoonlightTransactionBuilder", () => { validPublicKey, signature, expirationLedger, - nonce + nonce, ); // deno-lint-ignore no-explicit-any @@ -190,7 +190,7 @@ describe("MoonlightTransactionBuilder", () => { await testBuilder.signWithProvider( providerKeys, signatureExpirationLedger, - nonce + nonce, ); // deno-lint-ignore no-explicit-any @@ -198,7 +198,7 @@ describe("MoonlightTransactionBuilder", () => { assertEquals(providerSigs.size, 1); assertEquals( providerSigs.has(providerKeys.publicKey() as Ed25519PublicKey), - true + true, ); }); @@ -215,7 +215,7 @@ describe("MoonlightTransactionBuilder", () => { await testBuilder.signWithProvider( providerKeys, - signatureExpirationLedger + signatureExpirationLedger, ); // deno-lint-ignore no-explicit-any @@ -224,7 +224,7 @@ describe("MoonlightTransactionBuilder", () => { // Verify nonce was auto-generated const sigData = providerSigs.get( - providerKeys.publicKey() as Ed25519PublicKey + providerKeys.publicKey() as Ed25519PublicKey, ); assertExists(sigData.nonce); }); @@ -237,8 +237,8 @@ describe("MoonlightTransactionBuilder", () => { spendOperation.addCondition( Condition.create( (await generateP256KeyPair()).publicKey as UTXOPublicKey, - 1n - ) + 1n, + ), ); const testBuilder = new MoonlightTransactionBuilder({ channelId, @@ -254,7 +254,7 @@ describe("MoonlightTransactionBuilder", () => { await testBuilder.signWithSpendUtxo( utxoKeys, - signatureExpirationLedger + signatureExpirationLedger, ); // deno-lint-ignore no-explicit-any @@ -284,9 +284,8 @@ describe("MoonlightTransactionBuilder", () => { await testBuilder.signExtWithEd25519( userKeys, - signatureExpirationLedger, - nonce + nonce, ); // deno-lint-ignore no-explicit-any @@ -318,7 +317,7 @@ describe("MoonlightTransactionBuilder", () => { channelId, assetId, network, - nonce + nonce, ); // deno-lint-ignore no-explicit-any (testBuilder as any).addDeposit(operation); @@ -370,7 +369,7 @@ describe("MoonlightTransactionBuilder", () => { const authEntry = builder.getOperationAuthEntry( nonce, - signatureExpirationLedger + signatureExpirationLedger, ); assertExists(authEntry); @@ -383,7 +382,7 @@ describe("MoonlightTransactionBuilder", () => { const hash = await builder.getOperationAuthEntryHash( nonce, - signatureExpirationLedger + signatureExpirationLedger, ); assertExists(hash); @@ -449,7 +448,7 @@ describe("MoonlightTransactionBuilder", () => { await testBuilder.signWithProvider( providerKeys, - signatureExpirationLedger + signatureExpirationLedger, ); const signedEntry = testBuilder.getSignedOperationAuthEntry(); @@ -482,13 +481,12 @@ describe("MoonlightTransactionBuilder", () => { await testBuilder.signWithProvider( providerKeys, signatureExpirationLedger, - nonce + nonce, ); await testBuilder.signExtWithEd25519( userKeys, - signatureExpirationLedger, - nonce + nonce, ); const signedEntries = testBuilder.getSignedAuthEntries(); @@ -501,8 +499,8 @@ describe("MoonlightTransactionBuilder", () => { describe("Signatures XDR", () => { it("should generate signatures XDR when provider signatures exist", () => { - const pubKey = - LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + const pubKey = LocalSigner.generateRandom() + .publicKey() as Ed25519PublicKey; const signature = Buffer.from("provider_signature"); const expirationLedger = 1000000; const nonce = generateNonce(); @@ -518,7 +516,7 @@ describe("MoonlightTransactionBuilder", () => { pubKey, signature, expirationLedger, - nonce + nonce, ); const signaturesXdr = builderWithSigs.signaturesXDR(); @@ -554,13 +552,12 @@ describe("MoonlightTransactionBuilder", () => { await testBuilder.signWithProvider( providerKeys, signatureExpirationLedger, - nonce + nonce, ); await testBuilder.signExtWithEd25519( userKeys, - signatureExpirationLedger, - nonce + nonce, ); const invokeOp = testBuilder.getInvokeOperation(); @@ -582,7 +579,7 @@ describe("MoonlightTransactionBuilder", () => { await testBuilder.signWithProvider( providerKeys, - signatureExpirationLedger + signatureExpirationLedger, ); const invokeOp = testBuilder.getInvokeOperation(); @@ -616,13 +613,13 @@ describe("MoonlightTransactionBuilder", () => { assertThrows( () => testBuilder.addOperation(operation2), - TBU_ERR.DUPLICATE_CREATE_OP + TBU_ERR.DUPLICATE_CREATE_OP, ); }); it("should throw error for duplicate DEPOSIT operations", () => { - const pubKey = - LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + const pubKey = LocalSigner.generateRandom() + .publicKey() as Ed25519PublicKey; const condition = Condition.deposit(pubKey, validAmount); const operation1 = Operation.deposit(pubKey, validAmount); operation1.addCondition(condition); @@ -642,13 +639,13 @@ describe("MoonlightTransactionBuilder", () => { assertThrows( // deno-lint-ignore no-explicit-any () => (testBuilder as any).addDeposit(operation2), - TBU_ERR.DUPLICATE_DEPOSIT_OP + TBU_ERR.DUPLICATE_DEPOSIT_OP, ); }); it("should throw error for duplicate WITHDRAW operations", () => { - const pubKey = - LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + const pubKey = LocalSigner.generateRandom() + .publicKey() as Ed25519PublicKey; const condition = Condition.withdraw(pubKey, validAmount); const operation1 = Operation.withdraw(pubKey, validAmount); operation1.addCondition(condition); @@ -668,7 +665,7 @@ describe("MoonlightTransactionBuilder", () => { assertThrows( // deno-lint-ignore no-explicit-any () => (testBuilder as any).addWithdraw(operation2), - TBU_ERR.DUPLICATE_WITHDRAW_OP + TBU_ERR.DUPLICATE_WITHDRAW_OP, ); }); @@ -677,27 +674,27 @@ describe("MoonlightTransactionBuilder", () => { assertRejects( async () => await Operation.create(utxo, 0n), - OPR_ERR.AMOUNT_TOO_LOW + OPR_ERR.AMOUNT_TOO_LOW, ); }); it("should throw error for negative amount in DEPOSIT operation", () => { - const pubKey = - LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + const pubKey = LocalSigner.generateRandom() + .publicKey() as Ed25519PublicKey; assertThrows( () => Operation.deposit(pubKey, -100n), - OPR_ERR.AMOUNT_TOO_LOW + OPR_ERR.AMOUNT_TOO_LOW, ); }); it("should throw error for negative amount in WITHDRAW operation", () => { - const pubKey = - LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + const pubKey = LocalSigner.generateRandom() + .publicKey() as Ed25519PublicKey; assertThrows( () => Operation.withdraw(pubKey, -10n), - OPR_ERR.AMOUNT_TOO_LOW + OPR_ERR.AMOUNT_TOO_LOW, ); }); }); @@ -706,7 +703,7 @@ describe("MoonlightTransactionBuilder", () => { it("should throw error when adding inner signature without spend operation", async () => { const expirationLedger = 1000000; const nonExistentUtxo = new UTXOKeypairBase( - await generateP256KeyPair() + await generateP256KeyPair(), ); const testBuilder = new MoonlightTransactionBuilder({ @@ -719,15 +716,15 @@ describe("MoonlightTransactionBuilder", () => { await assertRejects( () => testBuilder.signWithSpendUtxo(nonExistentUtxo, expirationLedger), - TBU_ERR.NO_SPEND_OPS + TBU_ERR.NO_SPEND_OPS, ); }); it("should throw error when adding external signature without deposit/withdraw operation", () => { // deno-lint-ignore no-explicit-any const mockAuthEntry = {} as any; - const nonExistentKey = - LocalSigner.generateRandom().publicKey() as Ed25519PublicKey; + const nonExistentKey = LocalSigner.generateRandom() + .publicKey() as Ed25519PublicKey; const testBuilder = new MoonlightTransactionBuilder({ channelId, @@ -738,7 +735,7 @@ describe("MoonlightTransactionBuilder", () => { assertThrows( () => testBuilder.addExtSignedEntry(nonExistentKey, mockAuthEntry), - TBU_ERR.NO_EXT_OPS + TBU_ERR.NO_EXT_OPS, ); }); @@ -752,7 +749,7 @@ describe("MoonlightTransactionBuilder", () => { assertThrows( () => testBuilder.signaturesXDR(), - TBU_ERR.MISSING_PROVIDER_SIGNATURE + TBU_ERR.MISSING_PROVIDER_SIGNATURE, ); }); @@ -767,7 +764,7 @@ describe("MoonlightTransactionBuilder", () => { assertThrows( () => testBuilder.getSignedOperationAuthEntry(), Error, - "No Provider signatures added" + "No Provider signatures added", ); }); }); @@ -790,9 +787,9 @@ describe("MoonlightTransactionBuilder", () => { await testBuilder.signExtWithEd25519( nonExistentKey, signatureExpirationLedger, - nonce + nonce, ), - TBU_ERR.NO_DEPOSIT_OPS + TBU_ERR.NO_DEPOSIT_OPS, ); }); }); @@ -800,13 +797,13 @@ describe("MoonlightTransactionBuilder", () => { describe("Property Access Validation", () => { it("should throw error when accessing unset properties", () => { const emptyBuilder = Object.create( - MoonlightTransactionBuilder.prototype + MoonlightTransactionBuilder.prototype, ); assertThrows( // deno-lint-ignore no-explicit-any () => (emptyBuilder as any).require("_channelId"), - TBU_ERR.PROPERTY_NOT_SET + TBU_ERR.PROPERTY_NOT_SET, ); }); }); diff --git a/src/transaction-builder/signatures/signatures-xdr.ts b/src/transaction-builder/signatures/signatures-xdr.ts index 409742f..5d1b927 100644 --- a/src/transaction-builder/signatures/signatures-xdr.ts +++ b/src/transaction-builder/signatures/signatures-xdr.ts @@ -1,7 +1,11 @@ -import { xdr, StrKey } from "@stellar/stellar-sdk"; +import { StrKey, xdr } from "@stellar/stellar-sdk"; import { Buffer } from "buffer"; -export type SpendInnerSignature = { utxo: Uint8Array; sig: Buffer; exp: number }; +export type SpendInnerSignature = { + utxo: Uint8Array; + sig: Buffer; + exp: number; +}; export type ProviderInnerSignature = { pubKey: string; sig: Buffer; @@ -60,5 +64,3 @@ export const buildSignaturesXDR = ( const signatures = xdr.ScVal.scvVec([xdr.ScVal.scvMap(entries)]); return signatures.toXDR("base64"); }; - - diff --git a/src/transaction-builder/validators/operations.ts b/src/transaction-builder/validators/operations.ts index e9e0df4..75a68c1 100644 --- a/src/transaction-builder/validators/operations.ts +++ b/src/transaction-builder/validators/operations.ts @@ -5,76 +5,86 @@ import * as E from "../error.ts"; export const assertNoDuplicateCreate = ( existing: { getUtxo(): UTXOPublicKey }[], - op: { getUtxo(): UTXOPublicKey } + op: { getUtxo(): UTXOPublicKey }, ) => { if ( existing.find((c) => Buffer.from(c.getUtxo()).equals(Buffer.from(op.getUtxo())) ) - ) + ) { throw new E.DUPLICATE_CREATE_OP(op.getUtxo()); + } }; export const assertNoDuplicateSpend = ( existing: { getUtxo(): UTXOPublicKey }[], - op: { getUtxo(): UTXOPublicKey } + op: { getUtxo(): UTXOPublicKey }, ) => { if ( existing.find((s) => Buffer.from(s.getUtxo()).equals(Buffer.from(op.getUtxo())) ) - ) + ) { throw new E.DUPLICATE_SPEND_OP(op.getUtxo()); + } }; export const assertNoDuplicateDeposit = ( existing: { getPublicKey(): Ed25519PublicKey }[], - op: { getPublicKey(): Ed25519PublicKey } + op: { getPublicKey(): Ed25519PublicKey }, ) => { - if (existing.find((d) => d.getPublicKey() === op.getPublicKey())) + if (existing.find((d) => d.getPublicKey() === op.getPublicKey())) { throw new E.DUPLICATE_DEPOSIT_OP(op.getPublicKey()); + } }; export const assertNoDuplicateWithdraw = ( existing: { getPublicKey(): Ed25519PublicKey }[], - op: { getPublicKey(): Ed25519PublicKey } + op: { getPublicKey(): Ed25519PublicKey }, ) => { - if (existing.find((d) => d.getPublicKey() === op.getPublicKey())) + if (existing.find((d) => d.getPublicKey() === op.getPublicKey())) { throw new E.DUPLICATE_WITHDRAW_OP(op.getPublicKey()); + } }; export const assertSpendExists = ( existing: { getUtxo(): UTXOPublicKey }[], - utxo: UTXOPublicKey + utxo: UTXOPublicKey, ) => { - if (!existing.find((s) => Buffer.from(s.getUtxo()).equals(Buffer.from(utxo)))) + if ( + !existing.find((s) => Buffer.from(s.getUtxo()).equals(Buffer.from(utxo))) + ) { throw new E.NO_SPEND_OPS(utxo); + } }; export const assertExtOpsExist = ( existingDeposit: { getPublicKey(): Ed25519PublicKey }[], existingWithdraw: { getPublicKey(): Ed25519PublicKey }[], - pubKey: Ed25519PublicKey + pubKey: Ed25519PublicKey, ) => { if ( !existingDeposit.find((d) => d.getPublicKey() === pubKey) && !existingWithdraw.find((w) => w.getPublicKey() === pubKey) - ) + ) { throw new E.NO_EXT_OPS(pubKey); + } }; export const assertDepositExists = ( existing: { getPublicKey(): Ed25519PublicKey }[], - pubKey: Ed25519PublicKey + pubKey: Ed25519PublicKey, ) => { - if (!existing.find((d) => d.getPublicKey() === pubKey)) + if (!existing.find((d) => d.getPublicKey() === pubKey)) { throw new E.NO_DEPOSIT_OPS(pubKey); + } }; export const assertWithdrawExists = ( existing: { getPublicKey(): Ed25519PublicKey }[], - pubKey: Ed25519PublicKey + pubKey: Ed25519PublicKey, ) => { - if (!existing.find((d) => d.getPublicKey() === pubKey)) + if (!existing.find((d) => d.getPublicKey() === pubKey)) { throw new E.NO_WITHDRAW_OPS(pubKey); + } }; diff --git a/src/utils/assert/assert-args.ts b/src/utils/assert/assert-args.ts index 0437c1c..e6e2807 100644 --- a/src/utils/assert/assert-args.ts +++ b/src/utils/assert/assert-args.ts @@ -4,10 +4,11 @@ import type { MoonlightError } from "../../error/index.ts"; // Throws the provided error if any argument is invalid. export function assertRequiredArgs( args: Record, - errorFn: (argName: string) => MoonlightError + errorFn: (argName: string) => MoonlightError, ): asserts args is Record { for (const argName of Object.keys(args)) { - if (!(argName in args) || args[argName] === undefined) + if (!(argName in args) || args[argName] === undefined) { throw errorFn(argName); + } } } diff --git a/src/utils/conversion/numberToBytesBE.ts b/src/utils/conversion/numberToBytesBE.ts index 9c2cca3..b2871bf 100644 --- a/src/utils/conversion/numberToBytesBE.ts +++ b/src/utils/conversion/numberToBytesBE.ts @@ -3,7 +3,7 @@ export function numberToBytesBE(num: bigint, byteLength: number): Uint8Array { const bytes = new Uint8Array(byteLength); for (let i = 0; i < byteLength; i++) { bytes[byteLength - 1 - i] = Number( - (num >> (BigInt(8) * BigInt(i))) & BigInt(0xff) + (num >> (BigInt(8) * BigInt(i))) & BigInt(0xff), ); } return bytes; diff --git a/src/utils/secp256r1/deriveP256KeyPairFromSeed.ts b/src/utils/secp256r1/deriveP256KeyPairFromSeed.ts index 190f328..bd7f6a3 100644 --- a/src/utils/secp256r1/deriveP256KeyPairFromSeed.ts +++ b/src/utils/secp256r1/deriveP256KeyPairFromSeed.ts @@ -14,7 +14,7 @@ import { encodePKCS8 } from "./encodePKCS8.ts"; * @returns An object with publicKey and privateKey as Uint8Array. */ export async function deriveP256KeyPairFromSeed( - seed: Uint8Array + seed: Uint8Array, ): Promise<{ publicKey: Uint8Array; privateKey: Uint8Array }> { // Expand the seed to 48 bytes to eliminate bias (per FIPS 186-5 / RFC 9380) const info = "application"; // adjust as needed diff --git a/src/utils/secp256r1/encodeECPrivateKey.ts b/src/utils/secp256r1/encodeECPrivateKey.ts index bba6c21..476cf19 100644 --- a/src/utils/secp256r1/encodeECPrivateKey.ts +++ b/src/utils/secp256r1/encodeECPrivateKey.ts @@ -3,7 +3,7 @@ import * as asn1js from "asn1js"; // Helper: Encode ECPrivateKey (RFC 5915) export function encodeECPrivateKey( privateKey: Uint8Array, - publicKey: Uint8Array + publicKey: Uint8Array, ): Uint8Array { // Build the ECPrivateKey structure: // ECPrivateKey ::= SEQUENCE { diff --git a/src/utils/secp256r1/encodePKCS8.ts b/src/utils/secp256r1/encodePKCS8.ts index 807f7ed..6ab8dfa 100644 --- a/src/utils/secp256r1/encodePKCS8.ts +++ b/src/utils/secp256r1/encodePKCS8.ts @@ -3,7 +3,7 @@ import { encodeECPrivateKey } from "./encodeECPrivateKey.ts"; export function encodePKCS8( privateKey: Uint8Array, - publicKey: Uint8Array + publicKey: Uint8Array, ): Uint8Array { // First, encode the inner ECPrivateKey structure. const ecPrivateKeyDer = encodeECPrivateKey(privateKey, publicKey); diff --git a/src/utils/secp256r1/generateP256KeyPair.ts b/src/utils/secp256r1/generateP256KeyPair.ts index c5090fd..0e2dcec 100644 --- a/src/utils/secp256r1/generateP256KeyPair.ts +++ b/src/utils/secp256r1/generateP256KeyPair.ts @@ -10,13 +10,13 @@ export async function generateP256KeyPair(): Promise<{ namedCurve: "P-256", }, true, // Extractable - ["sign", "verify"] // Key usages + ["sign", "verify"], // Key usages ); // Export keys as raw and PKCS8 format const privateKey = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey); const publicKey = new Uint8Array( - await crypto.subtle.exportKey("raw", keyPair.publicKey) + await crypto.subtle.exportKey("raw", keyPair.publicKey), ); return { diff --git a/src/utils/secp256r1/signPayload.ts b/src/utils/secp256r1/signPayload.ts index 3000d55..5b8114d 100644 --- a/src/utils/secp256r1/signPayload.ts +++ b/src/utils/secp256r1/signPayload.ts @@ -2,7 +2,7 @@ import { Buffer } from "buffer"; export async function signPayload( payload: Uint8Array, - privateKeyBytes: Uint8Array + privateKeyBytes: Uint8Array, ): Promise { // Import the private key from the raw PKCS8 format const privateKey = await crypto.subtle.importKey( @@ -10,14 +10,14 @@ export async function signPayload( privateKeyBytes as BufferSource, // Raw private key bytes { name: "ECDSA", namedCurve: "P-256" }, // Algorithm details false, // Non-extractable - ["sign"] // Usage + ["sign"], // Usage ); // Sign the payload const signature = await crypto.subtle.sign( { name: "ECDSA", hash: { name: "SHA-256" } }, // Algorithm and hash privateKey, // Private key to sign with - payload as BufferSource // Data to sign + payload as BufferSource, // Data to sign ); // Convert signature to Uint8Array @@ -28,7 +28,7 @@ export async function signPayload( // **Assumption:** The signature is in raw R||S format (64 bytes) if (signatureBytes.length !== 64) { throw new Error( - `Unexpected signature format: expected 64 bytes, got ${signatureBytes.length}` + `Unexpected signature format: expected 64 bytes, got ${signatureBytes.length}`, ); } @@ -38,7 +38,7 @@ export async function signPayload( // Curve order Q for secp256r1 (P-256) const curveOrder = BigInt( - "0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551" + "0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", ); // Convert s to BigInt @@ -68,7 +68,7 @@ export async function signPayload( // Ensure sLowS is exactly 32 bytes if (sLowS.length !== 32) { throw new Error( - `Invalid normalized s length: expected 32 bytes, got ${sLowS.length}` + `Invalid normalized s length: expected 32 bytes, got ${sLowS.length}`, ); } diff --git a/src/utxo-based-account/error.ts b/src/utxo-based-account/error.ts index 5450221..15d9ca7 100644 --- a/src/utxo-based-account/error.ts +++ b/src/utxo-based-account/error.ts @@ -49,7 +49,8 @@ export class NEGATIVE_INDEX extends UTXOBasedAccountError { super({ code: Code.NEGATIVE_INDEX, message: `Negative index provided: ${index}`, - details: `The provided index ${index} is negative. Indices must be sequential non-negative integers.`, + details: + `The provided index ${index} is negative. Indices must be sequential non-negative integers.`, data: { index }, }); } @@ -60,7 +61,8 @@ export class UTXO_TO_DERIVE_TOO_LOW extends UTXOBasedAccountError { super({ code: Code.UTXO_TO_DERIVE_TOO_LOW, message: `UTXOs to derive too low: ${utxosToDerive}`, - details: `The number of UTXOs to derive must be at least 1. Provided value: ${utxosToDerive}.`, + details: + `The number of UTXOs to derive must be at least 1. Provided value: ${utxosToDerive}.`, data: { utxosToDerive }, }); } @@ -71,7 +73,8 @@ export class MISSING_BATCH_FETCH_FN extends UTXOBasedAccountError { super({ code: Code.MISSING_BATCH_FETCH_FN, message: `Missing batch fetch function`, - details: `A batch fetch function must be provided to retrieve UTXO public keys in batches.`, + details: + `A batch fetch function must be provided to retrieve UTXO public keys in batches.`, data: {}, }); } @@ -82,7 +85,8 @@ export class MISSING_UTXO_FOR_INDEX extends UTXOBasedAccountError { super({ code: Code.MISSING_UTXO_FOR_INDEX, message: `Missing UTXO for index: ${index}`, - details: `No UTXO public key found for the provided index ${index}. Ensure the index is valid and UTXOs have been derived up to this index.`, + details: + `No UTXO public key found for the provided index ${index}. Ensure the index is valid and UTXOs have been derived up to this index.`, data: { index }, }); } diff --git a/src/utxo-based-account/index.ts b/src/utxo-based-account/index.ts index a346121..dad9327 100644 --- a/src/utxo-based-account/index.ts +++ b/src/utxo-based-account/index.ts @@ -11,7 +11,7 @@ import { assert } from "../utils/assert/assert.ts"; export class UtxoBasedAccount< Context extends string, Root extends string, - Index extends `${number}` + Index extends `${number}`, > { private derivator: BaseDerivator; private readonly root: Root; @@ -24,7 +24,7 @@ export class UtxoBasedAccount< // Derived index: status -> Set of indices private statusIndex: Map> = new Map( - Object.values(UTXOStatus).map((status) => [status, new Set()]) + Object.values(UTXOStatus).map((status) => [status, new Set()]), ); // Track reserved UTXOs with timestamps (index -> timestamp) @@ -66,13 +66,13 @@ export class UtxoBasedAccount< } private createProxy( - utxo: UTXOKeypair + utxo: UTXOKeypair, ): UTXOKeypair { return new Proxy(utxo, { set: ( target: UTXOKeypair, prop: keyof UTXOKeypair, - value: unknown + value: unknown, ) => { const oldStatus = prop === "status" ? target[prop] : null; // @ts-ignore - we know the type is compatible @@ -113,7 +113,7 @@ export class UtxoBasedAccount< const utxoIndex = startIndex + i; const keypair = await UTXOKeypair.fromDerivator( this.derivator, - `${utxoIndex}` as Index + `${utxoIndex}` as Index, ); // Create proxied UTXO that automatically updates status index @@ -146,7 +146,7 @@ export class UtxoBasedAccount< const utxosToCheck = Array.from(this.utxos.entries()).filter( ([index, utxo]) => (!states || states.includes(utxo.status)) && - (!indices || indices.includes(index)) + (!indices || indices.includes(index)), ); // Process UTXOs in batches @@ -212,7 +212,7 @@ export class UtxoBasedAccount< */ private getFreeUTXOs(): UTXOKeypair[] { return this.getUTXOsByState(UTXOStatus.FREE).filter( - (utxo) => !this.isReserved(Number(utxo.index)) + (utxo) => !this.isReserved(Number(utxo.index)), ); } @@ -239,7 +239,7 @@ export class UtxoBasedAccount< */ selectUTXOsForTransfer( amount: bigint, - strategy: UTXOSelectionStrategy = UTXOSelectionStrategy.SEQUENTIAL + strategy: UTXOSelectionStrategy = UTXOSelectionStrategy.SEQUENTIAL, ): UTXOSelectionResult | null { const unspentUtxos = this.getUTXOsByState(UTXOStatus.UNSPENT); @@ -342,7 +342,7 @@ export class UtxoBasedAccount< return Array.from(this.reservations.keys()) .map((index) => this.getUTXO(index)) .filter( - (utxo): utxo is UTXOKeypair => utxo !== undefined + (utxo): utxo is UTXOKeypair => utxo !== undefined, ); } @@ -362,7 +362,7 @@ export class UtxoBasedAccount< * @returns Number of reservations released */ releaseStaleReservations( - maxAgeMs: number = this.maxReservationAgeMs + maxAgeMs: number = this.maxReservationAgeMs, ): number { const now = Date.now(); let released = 0; diff --git a/src/utxo-based-account/index.unit.test.ts b/src/utxo-based-account/index.unit.test.ts index 4218fe4..f146170 100644 --- a/src/utxo-based-account/index.unit.test.ts +++ b/src/utxo-based-account/index.unit.test.ts @@ -4,9 +4,9 @@ */ import { assertEquals, - assertThrows, - assertRejects, assertExists, + assertRejects, + assertThrows, } from "@std/assert"; import { UtxoBasedAccount } from "./index.ts"; import { UTXOStatus } from "../core/utxo-keypair/types.ts"; @@ -24,7 +24,7 @@ Deno.test("UtxoBasedAccount", async (t) => { const getBaseDerivator = () => new StellarDerivator().withNetworkAndContract( StellarNetworkId.Testnet, - TEST_CONTRACT_ID + TEST_CONTRACT_ID, ); await t.step("constructor should initialize with required parameters", () => { @@ -76,9 +76,9 @@ Deno.test("UtxoBasedAccount", async (t) => { assertEquals(utxos.length, 3); assertEquals( utxos.map((utxo) => Number(utxo.index)), - [1, 2, 3] + [1, 2, 3], ); - } + }, ); await t.step( @@ -94,9 +94,9 @@ Deno.test("UtxoBasedAccount", async (t) => { await assertRejects( async () => await account.batchLoad(), - UBA_ERR.MISSING_BATCH_FETCH_FN + UBA_ERR.MISSING_BATCH_FETCH_FN, ); - } + }, ); await t.step( @@ -119,7 +119,7 @@ Deno.test("UtxoBasedAccount", async (t) => { assertEquals(account.getUTXOsByState(UTXOStatus.UNSPENT).length, 2); assertEquals(account.getUTXOsByState(UTXOStatus.SPENT).length, 1); assertEquals(account.getUTXOsByState(UTXOStatus.FREE).length, 0); - } + }, ); await t.step( @@ -135,9 +135,9 @@ Deno.test("UtxoBasedAccount", async (t) => { assertThrows( () => account.updateUTXOState(999, UTXOStatus.UNSPENT), - UBA_ERR.MISSING_UTXO_FOR_INDEX + UBA_ERR.MISSING_UTXO_FOR_INDEX, ); - } + }, ); await t.step( @@ -163,7 +163,7 @@ Deno.test("UtxoBasedAccount", async (t) => { assertEquals(account.getUTXOsByState(UTXOStatus.FREE).length, 1); assertEquals(account.getUTXOsByState(UTXOStatus.UNSPENT).length, 1); assertEquals(account.getUTXOsByState(UTXOStatus.SPENT).length, 1); - } + }, ); await t.step( @@ -189,7 +189,7 @@ Deno.test("UtxoBasedAccount", async (t) => { assertEquals(selection.totalAmount, 300n); assertEquals(selection.changeAmount, 50n); assertEquals(selection.selectedUTXOs.length, 2); - } + }, ); await t.step( @@ -211,7 +211,7 @@ Deno.test("UtxoBasedAccount", async (t) => { const selection = account.selectUTXOsForTransfer(1000n); assertEquals(selection, null); - } + }, ); await t.step( @@ -226,7 +226,7 @@ Deno.test("UtxoBasedAccount", async (t) => { }); assertEquals(account.getTotalBalance(), 0n); - } + }, ); await t.step( @@ -251,7 +251,7 @@ Deno.test("UtxoBasedAccount", async (t) => { // - Third UTXO: 200n (UNSPENT) // - Fourth UTXO: 300n (UNSPENT) assertEquals(account.getTotalBalance(), 600n); - } + }, ); await t.step( @@ -277,6 +277,6 @@ Deno.test("UtxoBasedAccount", async (t) => { // Change one UTXO to SPENT state account.updateUTXOState(2, UTXOStatus.SPENT, 0n); assertEquals(account.getTotalBalance(), 400n); - } + }, ); }); diff --git a/src/utxo-based-account/utxo-based-stellar-account/index.ts b/src/utxo-based-account/utxo-based-stellar-account/index.ts index 3e3e104..a9768cf 100644 --- a/src/utxo-based-account/utxo-based-stellar-account/index.ts +++ b/src/utxo-based-account/utxo-based-stellar-account/index.ts @@ -1,8 +1,8 @@ import { UtxoBasedAccount } from "../index.ts"; import type { StellarDerivationContext, - StellarDerivationRoot, StellarDerivationIndex, + StellarDerivationRoot, } from "../../derivation/stellar/types.ts"; export class UtxoBasedStellarAccount extends UtxoBasedAccount< diff --git a/test/helpers/create-tx-invocation.ts b/test/helpers/create-tx-invocation.ts index b3c28e1..6c45813 100644 --- a/test/helpers/create-tx-invocation.ts +++ b/test/helpers/create-tx-invocation.ts @@ -12,7 +12,7 @@ export function createTxInvocation( options?: { fee?: string; timeout?: number; - } + }, ): TransactionInvocation { return { header: { diff --git a/test/integration/channel-auth.integration.test.ts b/test/integration/channel-auth.integration.test.ts index 83df8d2..de306fe 100644 --- a/test/integration/channel-auth.integration.test.ts +++ b/test/integration/channel-auth.integration.test.ts @@ -2,26 +2,26 @@ import { assertEquals, assertExists } from "@std/assert"; import { beforeAll, describe, it } from "@std/testing/bdd"; import { + Contract, + initializeWithFriendbot, LocalSigner, NativeAccount, TestNet, - initializeWithFriendbot, - Contract, } from "@colibri/core"; import type { + ContractId, Ed25519PublicKey, TransactionConfig, - ContractId, } from "@colibri/core"; import type { Buffer } from "node:buffer"; import { loadContractWasm } from "../helpers/load-wasm.ts"; import { - AuthSpec, - AuthReadMethods, AuthInvokeMethods, + AuthReadMethods, + AuthSpec, ChannelAuth, type ChannelTypes, } from "../../mod.ts"; @@ -33,7 +33,7 @@ describe("[Testnet - Integration] ChannelAuth", disableSanitizeConfig, () => { const admin = NativeAccount.fromMasterSigner(LocalSigner.generateRandom()); const providerA = NativeAccount.fromMasterSigner( - LocalSigner.generateRandom() + LocalSigner.generateRandom(), ); const txConfig: TransactionConfig = { @@ -48,12 +48,12 @@ describe("[Testnet - Integration] ChannelAuth", disableSanitizeConfig, () => { beforeAll(async () => { await initializeWithFriendbot( networkConfig.friendbotUrl, - admin.address() as Ed25519PublicKey + admin.address() as Ed25519PublicKey, ); await initializeWithFriendbot( networkConfig.friendbotUrl, - providerA.address() as Ed25519PublicKey + providerA.address() as Ed25519PublicKey, ); authWasm = loadContractWasm("channel_auth_contract"); diff --git a/test/integration/privacy-channel.integration.test.ts b/test/integration/privacy-channel.integration.test.ts index 75ef6b5..7d81dc3 100644 --- a/test/integration/privacy-channel.integration.test.ts +++ b/test/integration/privacy-channel.integration.test.ts @@ -2,20 +2,20 @@ import { assertEquals, assertExists } from "@std/assert"; import { beforeAll, describe, it } from "@std/testing/bdd"; import { + Contract, + initializeWithFriendbot, LocalSigner, NativeAccount, - TestNet, - initializeWithFriendbot, - Contract, P_SimulateTransactionErrors, + TestNet, } from "@colibri/core"; import type { - Ed25519PublicKey, - TransactionConfig, ContractId, - TestNetConfig, + Ed25519PublicKey, Ed25519SecretKey, + TestNetConfig, + TransactionConfig, } from "@colibri/core"; import { AuthInvokeMethods, @@ -24,15 +24,15 @@ import { import type { Buffer } from "node:buffer"; import { loadContractWasm } from "../helpers/load-wasm.ts"; import { + ChannelInvokeMethods, + ChannelReadMethods, ChannelSpec, type ChannelTypes, - PrivacyChannel, - ChannelReadMethods, + generateNonce, generateP256KeyPair, - MoonlightTransactionBuilder, MoonlightOperation as op, - generateNonce, - ChannelInvokeMethods, + MoonlightTransactionBuilder, + PrivacyChannel, } from "../../mod.ts"; import { Asset, Keypair } from "@stellar/stellar-sdk"; @@ -52,11 +52,11 @@ describe( const johnKeys = Keypair.random(); const providerA = NativeAccount.fromMasterSigner( - LocalSigner.fromSecret(providerKeys.secret() as Ed25519SecretKey) + LocalSigner.fromSecret(providerKeys.secret() as Ed25519SecretKey), ); const john = NativeAccount.fromMasterSigner( - LocalSigner.fromSecret(johnKeys.secret() as Ed25519SecretKey) + LocalSigner.fromSecret(johnKeys.secret() as Ed25519SecretKey), ); const txConfig: TransactionConfig = { @@ -67,7 +67,7 @@ describe( }; const assetId = Asset.native().contractId( - networkConfig.networkPassphrase + networkConfig.networkPassphrase, ) as ContractId; let rpc: Server; @@ -80,17 +80,17 @@ describe( beforeAll(async () => { await initializeWithFriendbot( networkConfig.friendbotUrl, - admin.address() as Ed25519PublicKey + admin.address() as Ed25519PublicKey, ); await initializeWithFriendbot( networkConfig.friendbotUrl, - providerA.address() as Ed25519PublicKey + providerA.address() as Ed25519PublicKey, ); await initializeWithFriendbot( networkConfig.friendbotUrl, - john.address() as Ed25519PublicKey + john.address() as Ed25519PublicKey, ); rpc = new Server(networkConfig.rpcUrl as string); @@ -159,7 +159,7 @@ describe( networkConfig, channelId, authId, - assetId + assetId, ); assertExists(channelClient); @@ -179,7 +179,7 @@ describe( networkConfig, channelId, authId, - assetId + assetId, ); const utxoKeypair = generateP256KeyPair(); @@ -226,7 +226,7 @@ describe( networkConfig, channelId, authId, - assetId + assetId, ); const utxoAKeypair = await generateP256KeyPair(); @@ -237,7 +237,7 @@ describe( channelId: channelId, authId: authId, assetId: Asset.native().contractId( - networkConfig.networkPassphrase + networkConfig.networkPassphrase, ) as ContractId, }); @@ -249,7 +249,7 @@ describe( depositTx.addOperation( op .deposit(john.address() as Ed25519PublicKey, 500n) - .addConditions([createOpA.toCondition(), createOpB.toCondition()]) + .addConditions([createOpA.toCondition(), createOpB.toCondition()]), ); const latestLedger = await rpc.getLatestLedger(); @@ -261,13 +261,13 @@ describe( await depositTx.signExtWithEd25519( johnKeys, signatureExpirationLedger, - nonce + nonce, ); await depositTx.signWithProvider( providerKeys, signatureExpirationLedger, - nonce + nonce, ); await channelClient @@ -284,7 +284,7 @@ describe( console.error("Error invoking contract:", e); console.error( "Transaction XDR:", - e.meta.data.input.transaction.toXDR() + e.meta.data.input.transaction.toXDR(), ); } throw e; @@ -306,5 +306,5 @@ describe( assertEquals(utxoBBal, 250n); }); }); - } + }, ); diff --git a/test/integration/utxo-based-account.integration.test.ts b/test/integration/utxo-based-account.integration.test.ts index 922b02e..05874dc 100644 --- a/test/integration/utxo-based-account.integration.test.ts +++ b/test/integration/utxo-based-account.integration.test.ts @@ -3,17 +3,17 @@ import { assertEquals, assertExists } from "@std/assert"; import { beforeAll, describe, it } from "@std/testing/bdd"; import { + Contract, + type ContractId, + type Ed25519PublicKey, + type Ed25519SecretKey, + initializeWithFriendbot, LocalSigner, NativeAccount, - TestNet, - initializeWithFriendbot, - Contract, P_SimulateTransactionErrors, - type Ed25519PublicKey, - type TransactionConfig, - type ContractId, + TestNet, type TestNetConfig, - type Ed25519SecretKey, + type TransactionConfig, } from "@colibri/core"; import { Buffer } from "node:buffer"; @@ -23,20 +23,20 @@ import { Server } from "@stellar/stellar-sdk/rpc"; import { disableSanitizeConfig } from "../utils/disable-sanitize-config.ts"; import { - AuthSpec, AuthInvokeMethods, + AuthSpec, ChannelInvokeMethods, ChannelReadMethods, ChannelSpec, - UTXOStatus, - MoonlightTransactionBuilder, - MoonlightOperation as op, + type ChannelTypes, generateNonce, + MoonlightOperation as op, + MoonlightTransactionBuilder, + PrivacyChannel, StellarDerivator, StellarNetworkId, UtxoBasedStellarAccount, - type ChannelTypes, - PrivacyChannel, + UTXOStatus, } from "../../mod.ts"; describe( @@ -50,11 +50,11 @@ describe( const userKeys = Keypair.random(); const provider = NativeAccount.fromMasterSigner( - LocalSigner.fromSecret(providerKeys.secret() as Ed25519SecretKey) + LocalSigner.fromSecret(providerKeys.secret() as Ed25519SecretKey), ); const user = NativeAccount.fromMasterSigner( - LocalSigner.fromSecret(userKeys.secret() as Ed25519SecretKey) + LocalSigner.fromSecret(userKeys.secret() as Ed25519SecretKey), ); const txConfig: TransactionConfig = { @@ -65,7 +65,7 @@ describe( }; const assetId = Asset.native().contractId( - networkConfig.networkPassphrase + networkConfig.networkPassphrase, ) as ContractId; let authWasm: Buffer; @@ -79,17 +79,17 @@ describe( // Initialize accounts with friendbot await initializeWithFriendbot( networkConfig.friendbotUrl, - admin.address() as Ed25519PublicKey + admin.address() as Ed25519PublicKey, ); await initializeWithFriendbot( networkConfig.friendbotUrl, - provider.address() as Ed25519PublicKey + provider.address() as Ed25519PublicKey, ); await initializeWithFriendbot( networkConfig.friendbotUrl, - user.address() as Ed25519PublicKey + user.address() as Ed25519PublicKey, ); // Load contract WASMs @@ -162,7 +162,7 @@ describe( networkConfig, channelId, authId, - assetId + assetId, ); }); @@ -208,7 +208,7 @@ describe( // Create a fresh derivator for this test const stellarDerivator = new StellarDerivator().withNetworkAndContract( StellarNetworkId.Testnet, - channelId + channelId, ); const utxoAccount = new UtxoBasedStellarAccount({ @@ -234,7 +234,7 @@ describe( assertEquals( freeUtxos.length, batchSize, - "Should have derived the correct number of UTXOs" + "Should have derived the correct number of UTXOs", ); // Verify each UTXO has required properties @@ -257,7 +257,7 @@ describe( // Create a fresh derivator for this test const freshDerivator = new StellarDerivator().withNetworkAndContract( StellarNetworkId.Testnet, - channelId + channelId, ); const utxoAccount = new UtxoBasedStellarAccount({ @@ -290,7 +290,7 @@ describe( channelId: channelId, authId: authId, assetId: Asset.native().contractId( - networkConfig.networkPassphrase + networkConfig.networkPassphrase, ) as ContractId, }); @@ -299,7 +299,7 @@ describe( depositTx.addOperation( op .deposit(user.address() as Ed25519PublicKey, depositAmount) - .addConditions([createOp.toCondition()]) + .addConditions([createOp.toCondition()]), ); // Get latest ledger for signature expiration @@ -311,13 +311,13 @@ describe( await depositTx.signExtWithEd25519( userKeys, signatureExpirationLedger, - nonce + nonce, ); await depositTx.signWithProvider( providerKeys, signatureExpirationLedger, - nonce + nonce, ); // Execute the deposit transaction @@ -335,7 +335,7 @@ describe( console.error("Error invoking contract:", e); console.error( "Transaction XDR:", - e.meta.data.input.transaction.toXDR() + e.meta.data.input.transaction.toXDR(), ); } throw e; @@ -355,7 +355,7 @@ describe( assertEquals( balanceResult, depositAmount, - "UTXO balance should match the deposited amount" + "UTXO balance should match the deposited amount", ); // Verify the UTXO state changed to UNSPENT @@ -366,7 +366,7 @@ describe( assertEquals( unspentUtxo.balance, depositAmount, - "UTXO should have correct balance" + "UTXO should have correct balance", ); }); @@ -376,7 +376,7 @@ describe( // Create a fresh derivator for this test const stellarDerivator = new StellarDerivator().withNetworkAndContract( StellarNetworkId.Testnet, - channelId + channelId, ); const utxoAccount = new UtxoBasedStellarAccount({ @@ -413,7 +413,7 @@ describe( channelId: channelId, authId: authId, assetId: Asset.native().contractId( - networkConfig.networkPassphrase + networkConfig.networkPassphrase, ) as ContractId, }); @@ -422,7 +422,7 @@ describe( depositTx.addOperation( op .deposit(user.address() as Ed25519PublicKey, amount) - .addConditions([createOp.toCondition()]) + .addConditions([createOp.toCondition()]), ); const latestLedger = await rpc.getLatestLedger(); @@ -432,13 +432,13 @@ describe( await depositTx.signExtWithEd25519( userKeys, signatureExpirationLedger, - nonce + nonce, ); await depositTx.signWithProvider( providerKeys, signatureExpirationLedger, - nonce + nonce, ); await channelClient.invokeRaw({ @@ -463,7 +463,7 @@ describe( assertEquals( unspentUtxos[i].balance, amounts[i], - `UTXO ${i} should have correct balance` + `UTXO ${i} should have correct balance`, ); } @@ -472,7 +472,7 @@ describe( assertEquals( totalBalance, expectedTotal, - "Total balance should be sum of all UNSPENT UTXOs" + "Total balance should be sum of all UNSPENT UTXOs", ); }); @@ -483,7 +483,7 @@ describe( // Create a fresh derivator for this test const stellarDerivator = new StellarDerivator().withNetworkAndContract( StellarNetworkId.Testnet, - channelId + channelId, ); const utxoAccount = new UtxoBasedStellarAccount({ @@ -515,7 +515,7 @@ describe( channelId: channelId, authId: authId, assetId: Asset.native().contractId( - networkConfig.networkPassphrase + networkConfig.networkPassphrase, ) as ContractId, }); @@ -524,7 +524,7 @@ describe( depositTx.addOperation( op .deposit(user.address() as Ed25519PublicKey, depositAmount) - .addConditions([createOp.toCondition()]) + .addConditions([createOp.toCondition()]), ); const latestLedger = await rpc.getLatestLedger(); @@ -534,13 +534,13 @@ describe( await depositTx.signExtWithEd25519( userKeys, signatureExpirationLedger, - nonce + nonce, ); await depositTx.signWithProvider( providerKeys, signatureExpirationLedger, - nonce + nonce, ); await channelClient.invokeRaw({ @@ -569,7 +569,7 @@ describe( assertEquals( balanceBeforeWithdraw, depositAmount, - "UTXO should have balance before withdraw" + "UTXO should have balance before withdraw", ); // Now withdraw from the UTXO @@ -578,7 +578,7 @@ describe( channelId: channelId, authId: authId, assetId: Asset.native().contractId( - networkConfig.networkPassphrase + networkConfig.networkPassphrase, ) as ContractId, }); @@ -595,7 +595,7 @@ describe( await withdrawTx.signWithProvider( providerKeys, signatureExpirationLedger, - generateNonce() + generateNonce(), ); // Execute the withdraw transaction @@ -613,7 +613,7 @@ describe( console.error("Error invoking withdraw contract:", e); console.error( "Transaction XDR:", - e.meta.data.input.transaction.toXDR() + e.meta.data.input.transaction.toXDR(), ); } throw e; @@ -633,7 +633,7 @@ describe( assertEquals( balanceResult, 0n, - "UTXO balance should be 0 after withdrawal" + "UTXO balance should be 0 after withdrawal", ); // Verify the UTXO state changed to SPENT @@ -650,7 +650,7 @@ describe( // Create a fresh derivator for this test const stellarDerivator = new StellarDerivator().withNetworkAndContract( StellarNetworkId.Testnet, - channelId + channelId, ); const utxoAccount = new UtxoBasedStellarAccount({ @@ -686,7 +686,7 @@ describe( channelId: channelId, authId: authId, assetId: Asset.native().contractId( - networkConfig.networkPassphrase + networkConfig.networkPassphrase, ) as ContractId, }); @@ -695,7 +695,7 @@ describe( depositTx.addOperation( op .deposit(user.address() as Ed25519PublicKey, amount) - .addConditions([createOp.toCondition()]) + .addConditions([createOp.toCondition()]), ); const latestLedger = await rpc.getLatestLedger(); @@ -705,13 +705,13 @@ describe( await depositTx.signExtWithEd25519( userKeys, signatureExpirationLedger, - nonce + nonce, ); await depositTx.signWithProvider( providerKeys, signatureExpirationLedger, - nonce + nonce, ); await channelClient.invokeRaw({ @@ -732,7 +732,7 @@ describe( assertEquals( unspentUtxos.length, 2, - "Should have 2 UNSPENT UTXOs after batchLoad" + "Should have 2 UNSPENT UTXOs after batchLoad", ); // Verify that 3 UTXOs are still FREE (no balances) @@ -740,7 +740,7 @@ describe( assertEquals( freeUtxosAfterLoad.length, 3, - "Should have 3 FREE UTXOs after batchLoad" + "Should have 3 FREE UTXOs after batchLoad", ); // Verify the balances are correct @@ -748,7 +748,7 @@ describe( assertEquals( unspentUtxos[i].balance, amounts[i], - `UTXO ${i} should have correct balance after batchLoad` + `UTXO ${i} should have correct balance after batchLoad`, ); } @@ -758,7 +758,7 @@ describe( assertEquals( totalBalance, expectedTotal, - "Total balance should be sum of UNSPENT UTXOs" + "Total balance should be sum of UNSPENT UTXOs", ); }); }); @@ -770,7 +770,7 @@ describe( // Create a fresh derivator for this test const stellarDerivator = new StellarDerivator().withNetworkAndContract( StellarNetworkId.Testnet, - channelId + channelId, ); const utxoAccount = new UtxoBasedStellarAccount({ @@ -806,7 +806,7 @@ describe( channelId: channelId, authId: authId, assetId: Asset.native().contractId( - networkConfig.networkPassphrase + networkConfig.networkPassphrase, ) as ContractId, }); @@ -815,7 +815,7 @@ describe( depositTx.addOperation( op .deposit(user.address() as Ed25519PublicKey, amount) - .addConditions([createOp.toCondition()]) + .addConditions([createOp.toCondition()]), ); const latestLedger = await rpc.getLatestLedger(); @@ -825,13 +825,13 @@ describe( await depositTx.signExtWithEd25519( userKeys, signatureExpirationLedger, - nonce + nonce, ); await depositTx.signWithProvider( providerKeys, signatureExpirationLedger, - nonce + nonce, ); await channelClient.invokeRaw({ @@ -848,25 +848,25 @@ describe( // Step 3: Verify states after deposits (3 UNSPENT, 2 FREE) const unspentAfterDeposits = utxoAccount.getUTXOsByState( - UTXOStatus.UNSPENT + UTXOStatus.UNSPENT, ); const freeAfterDeposits = utxoAccount.getUTXOsByState(UTXOStatus.FREE); assertEquals( unspentAfterDeposits.length, 3, - "Should have 3 UNSPENT UTXOs after deposits" + "Should have 3 UNSPENT UTXOs after deposits", ); assertEquals( freeAfterDeposits.length, 2, - "Should have 2 FREE UTXOs after deposits" + "Should have 2 FREE UTXOs after deposits", ); const totalAfterDeposits = utxoAccount.getTotalBalance(); assertEquals( totalAfterDeposits, 600000n, - "Total balance should be 600000 after deposits" + "Total balance should be 600000 after deposits", ); // Step 4: Withdraw from first 2 UTXOs @@ -879,7 +879,7 @@ describe( channelId: channelId, authId: authId, assetId: Asset.native().contractId( - networkConfig.networkPassphrase + networkConfig.networkPassphrase, ) as ContractId, }); @@ -894,13 +894,13 @@ describe( await withdrawTx.signWithSpendUtxo( testUtxo, - signatureExpirationLedger + signatureExpirationLedger, ); await withdrawTx.signWithProvider( providerKeys, signatureExpirationLedger, - generateNonce() + generateNonce(), ); await channelClient.invokeRaw({ @@ -917,34 +917,34 @@ describe( // Step 5: Verify states after withdraws (1 UNSPENT, 2 SPENT, 2 FREE) const unspentAfterWithdraws = utxoAccount.getUTXOsByState( - UTXOStatus.UNSPENT + UTXOStatus.UNSPENT, ); const spentAfterWithdraws = utxoAccount.getUTXOsByState( - UTXOStatus.SPENT + UTXOStatus.SPENT, ); const freeAfterWithdraws = utxoAccount.getUTXOsByState(UTXOStatus.FREE); assertEquals( unspentAfterWithdraws.length, 1, - "Should have 1 UNSPENT UTXO after withdraws" + "Should have 1 UNSPENT UTXO after withdraws", ); assertEquals( spentAfterWithdraws.length, 2, - "Should have 2 SPENT UTXOs after withdraws" + "Should have 2 SPENT UTXOs after withdraws", ); assertEquals( freeAfterWithdraws.length, 2, - "Should have 2 FREE UTXOs after withdraws" + "Should have 2 FREE UTXOs after withdraws", ); const totalAfterWithdraws = utxoAccount.getTotalBalance(); assertEquals( totalAfterWithdraws, 300000n, - "Total balance should be 300000 after withdraws (only third UTXO)" + "Total balance should be 300000 after withdraws (only third UTXO)", ); // Step 6: Make new deposit to one of the FREE UTXOs @@ -956,7 +956,7 @@ describe( channelId: channelId, authId: authId, assetId: Asset.native().contractId( - networkConfig.networkPassphrase + networkConfig.networkPassphrase, ) as ContractId, }); @@ -965,7 +965,7 @@ describe( newDepositTx.addOperation( op .deposit(user.address() as Ed25519PublicKey, newDepositAmount) - .addConditions([createOp.toCondition()]) + .addConditions([createOp.toCondition()]), ); const latestLedger = await rpc.getLatestLedger(); @@ -975,13 +975,13 @@ describe( await newDepositTx.signExtWithEd25519( userKeys, signatureExpirationLedger, - nonce + nonce, ); await newDepositTx.signWithProvider( providerKeys, signatureExpirationLedger, - nonce + nonce, ); await channelClient.invokeRaw({ @@ -1003,7 +1003,7 @@ describe( assertEquals( finalUnspent.length, 2, - "Should have 2 UNSPENT UTXOs at end" + "Should have 2 UNSPENT UTXOs at end", ); assertEquals(finalSpent.length, 2, "Should have 2 SPENT UTXOs at end"); assertEquals(finalFree.length, 1, "Should have 1 FREE UTXO at end"); @@ -1013,21 +1013,21 @@ describe( assertEquals( finalTotal, expectedFinalTotal, - "Final balance should be sum of remaining UNSPENT UTXOs" + "Final balance should be sum of remaining UNSPENT UTXOs", ); // Verify individual balances assertEquals( finalUnspent[0].balance, 300000n, - "First UNSPENT should have 300000" + "First UNSPENT should have 300000", ); assertEquals( finalUnspent[1].balance, 150000n, - "Second UNSPENT should have 150000" + "Second UNSPENT should have 150000", ); }); }); - } + }, ); diff --git a/test/utils/traverse-object-log.ts b/test/utils/traverse-object-log.ts index e9c4302..5437260 100644 --- a/test/utils/traverse-object-log.ts +++ b/test/utils/traverse-object-log.ts @@ -2,7 +2,7 @@ export function traverseObjectLog( // deno-lint-ignore no-explicit-any obj: any, options: { maxDepth?: number; nodeThreshold?: number } = {}, - currentDepth: number = 0 + currentDepth: number = 0, ): void { const { maxDepth = Infinity, nodeThreshold = Infinity } = options; @@ -10,7 +10,7 @@ export function traverseObjectLog( if (currentDepth === maxDepth) { console.log( " ".repeat(currentDepth * 2) + `Max depth reached. Value:`, - obj + obj, ); return; } @@ -27,7 +27,7 @@ export function traverseObjectLog( // If the number of keys exceeds the threshold, log the count and return. if (keys.length > nodeThreshold) { console.log( - " ".repeat(currentDepth * 2) + `Branch has ${keys.length} nodes` + " ".repeat(currentDepth * 2) + `Branch has ${keys.length} nodes`, ); return; } From ab19a466226399d3a62647f1c828c55b19a2cc46 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Tue, 11 Nov 2025 16:56:42 -0300 Subject: [PATCH 75/90] refactor: update VSCode settings to enable format on save and set default formatter --- .vscode/settings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 20a5837..293e41e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { "deno.enable": true, + "editor.formatOnSave": true, + "editor.defaultFormatter": "denoland.vscode-deno", "github.copilot.editor.enableCodeActions": true, "github.copilot.advanced": { "instructionPath": ".github/copilot-instructions.md" From 570c12a18e14dafeac97839262614fde3c7d5f3a Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Tue, 11 Nov 2025 18:01:48 -0300 Subject: [PATCH 76/90] fix: replace deprecated deposit-auth-entry copy with updated implementation - Updated import statement in src/operation/index.ts to reference the correct deposit-auth-entry.ts file. - Deleted obsolete src/utils/auth/deposit-auth-entry copy.ts file. - Renamed function in src/utils/auth/deposit-auth-entry.ts from generateDepositAuthEntry to buildDepositAuthEntry for consistency. --- src/operation/index.ts | 2 +- src/utils/auth/deposit-auth-entry copy.ts | 14 -------------- src/utils/auth/deposit-auth-entry.ts | 2 +- 3 files changed, 2 insertions(+), 16 deletions(-) delete mode 100644 src/utils/auth/deposit-auth-entry copy.ts diff --git a/src/operation/index.ts b/src/operation/index.ts index fa5a335..bd8cc28 100644 --- a/src/operation/index.ts +++ b/src/operation/index.ts @@ -34,7 +34,7 @@ import * as E from "./error.ts"; import { assert } from "../utils/assert/assert.ts"; import { buildAuthPayloadHash } from "../utils/auth/build-auth-payload.ts"; import { generateNonce } from "../utils/common/index.ts"; -import { buildDepositAuthEntry } from "../utils/auth/deposit-auth-entry copy.ts"; +import { buildDepositAuthEntry } from "../utils/auth/deposit-auth-entry.ts"; import { MLXDR } from "../custom-xdr/index.ts"; export class MoonlightOperation implements BaseOperation { diff --git a/src/utils/auth/deposit-auth-entry copy.ts b/src/utils/auth/deposit-auth-entry copy.ts deleted file mode 100644 index d32c39e..0000000 --- a/src/utils/auth/deposit-auth-entry copy.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { xdr } from "@stellar/stellar-sdk"; -import { generateDepositAuthEntry } from "../../utils/auth/deposit-auth-entry.ts"; - -export const buildDepositAuthEntry = (args: { - channelId: string; - assetId: string; - depositor: string; - amount: bigint; - conditions: xdr.ScVal[]; - nonce: string; - signatureExpirationLedger: number; -}): xdr.SorobanAuthorizationEntry => { - return generateDepositAuthEntry(args); -}; diff --git a/src/utils/auth/deposit-auth-entry.ts b/src/utils/auth/deposit-auth-entry.ts index c70e289..7bcdad4 100644 --- a/src/utils/auth/deposit-auth-entry.ts +++ b/src/utils/auth/deposit-auth-entry.ts @@ -1,7 +1,7 @@ import type { xdr } from "@stellar/stellar-sdk"; import { xdr as xdrHelper } from "@colibri/core"; -export const generateDepositAuthEntry = ({ +export const buildDepositAuthEntry = ({ channelId, assetId, depositor, From 550655001839950614713c421fd42973bd2aa01f Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 13 Nov 2025 08:51:58 -0300 Subject: [PATCH 77/90] feat: add getBalancesFetcher method to PrivacyChannel for UTXO balance retrieval - Introduced a new method `getBalancesFetcher` in the `PrivacyChannel` class. - This method returns a function that fetches balances for given UTXO public keys. - Updated imports to include `UTXOPublicKey` and `Buffer` for handling UTXO data. - Enhanced readability by updating comments in the code. --- src/privacy-channel/index.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index 61a96eb..691bfc7 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -8,12 +8,14 @@ import { StellarDerivator } from "../derivation/stellar/index.ts"; import type { StellarNetworkId } from "../derivation/stellar/stellar-network-id.ts"; import { type ChannelInvokeMethods, - type ChannelReadMethods, + ChannelReadMethods, ChannelSpec, } from "./constants.ts"; import type { ChannelInvoke, ChannelRead } from "./types.ts"; import type { xdr } from "@stellar/stellar-sdk"; import * as E from "./error.ts"; +import type { UTXOPublicKey } from "@moonlight/moonlight-sdk"; +import { Buffer } from "buffer"; export class PrivacyChannel { private _client: Contract; @@ -142,8 +144,24 @@ export class PrivacyChannel { return this.getClient().getContractId(); } + /** + * Returns a function that fetches balances for given UTXO public keys. + * + * @returns {(publicKeys: UTXOPublicKey[]) => Promise>} + */ + public getBalancesFetcher() { + const fetchBalances = (publicKeys: UTXOPublicKey[]) => { + return this.read({ + method: ChannelReadMethods.utxo_balances, + methodArgs: { utxos: publicKeys.map((pk) => Buffer.from(pk)) }, + }); + }; + + return fetchBalances; + } + //========================================== - // Read / Write Methods + // Contract Read / Write Methods //========================================== // // From 091a86ced6ae1641c8a4b4ebf331a50b0d6e8a5b Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 13 Nov 2025 08:55:14 -0300 Subject: [PATCH 78/90] feat: add getTransactionBuilder method to PrivacyChannel for creating pre-configured transaction builders --- src/privacy-channel/index.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index 691bfc7..0c87aca 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -16,6 +16,7 @@ import type { xdr } from "@stellar/stellar-sdk"; import * as E from "./error.ts"; import type { UTXOPublicKey } from "@moonlight/moonlight-sdk"; import { Buffer } from "buffer"; +import { MoonlightTransactionBuilder } from "../transaction-builder/index.ts"; export class PrivacyChannel { private _client: Contract; @@ -160,6 +161,23 @@ export class PrivacyChannel { return fetchBalances; } + /** + * Creates and returns a MoonlightTransactionBuilder instance + * pre-configured for this privacy channel. + * + * @returns {MoonlightTransactionBuilder} A pre-configured MoonlightTransactionBuilder instance. + */ + public getTransactionBuilder() { + const txBuilder = new MoonlightTransactionBuilder({ + channelId: this.getChannelId(), + authId: this.getAuthId(), + network: this.getNetworkConfig().networkPassphrase, + assetId: this.getAssetId(), + }); + + return txBuilder; + } + //========================================== // Contract Read / Write Methods //========================================== From 32f0635183c4b8476d754de461ec10ef13cb5748 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 13 Nov 2025 08:55:22 -0300 Subject: [PATCH 79/90] fix: correct formatting in JSDoc for getTransactionBuilder method in PrivacyChannel --- src/privacy-channel/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index 0c87aca..68e907a 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -165,7 +165,7 @@ export class PrivacyChannel { * Creates and returns a MoonlightTransactionBuilder instance * pre-configured for this privacy channel. * - * @returns {MoonlightTransactionBuilder} A pre-configured MoonlightTransactionBuilder instance. + * @returns {MoonlightTransactionBuilder} A pre-configured MoonlightTransactionBuilder instance. */ public getTransactionBuilder() { const txBuilder = new MoonlightTransactionBuilder({ From dfcdd12eb5754195030f82c79616ef2b8f72ee22 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 13 Nov 2025 09:06:21 -0300 Subject: [PATCH 80/90] feat: add getUTXOAccountHandler method to PrivacyChannel for UTXO account management - Implemented getUTXOAccountHandler in PrivacyChannel to create and return a UtxoBasedStellarAccount handler pre-configured for the privacy channel. - Added GetUTXOAccountHandlerArgs type to define the arguments required for the new method. - Updated types.ts to include Ed25519SecretKey in imports and defined the new GetUTXOAccountHandlerArgs type. - Modified index.ts in utxo-based-account to adjust the constructor to accept UTXOBasedAccountContructorArgs. --- src/privacy-channel/index.ts | 36 ++++++++++++++++++++++++++++++++- src/privacy-channel/types.ts | 23 ++++++++++++++++++++- src/utxo-based-account/index.ts | 16 +++++---------- src/utxo-based-account/types.ts | 16 +++++++++++++++ 4 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index 68e907a..94bce65 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -1,6 +1,7 @@ import { Contract, type ContractId, + Ed25519SecretKey, type NetworkConfig, type TransactionConfig, } from "@colibri/core"; @@ -11,12 +12,17 @@ import { ChannelReadMethods, ChannelSpec, } from "./constants.ts"; -import type { ChannelInvoke, ChannelRead } from "./types.ts"; +import type { + ChannelInvoke, + ChannelRead, + GetUTXOAccountHandlerArgs, +} from "./types.ts"; import type { xdr } from "@stellar/stellar-sdk"; import * as E from "./error.ts"; import type { UTXOPublicKey } from "@moonlight/moonlight-sdk"; import { Buffer } from "buffer"; import { MoonlightTransactionBuilder } from "../transaction-builder/index.ts"; +import { UtxoBasedStellarAccount } from "../utxo-based-account/utxo-based-stellar-account/index.ts"; export class PrivacyChannel { private _client: Contract; @@ -178,6 +184,34 @@ export class PrivacyChannel { return txBuilder; } + /** + * Creates and returns a UtxoBasedStellarAccount handler + * pre-configured for this privacy channel. + * + * @param {GetUTXOAccountHandlerArgs} args - The arguments for creating the UTXO account handler. + * @param {Ed25519SecretKey} args.root - The root secret key for the Stellar account. + * @param {Object} [args.options] - Additional options for the UTXO account handler. + * @returns + */ + public getUTXOAccountHandler(args: GetUTXOAccountHandlerArgs) { + const { root, options } = args; + + const derivator = new StellarDerivator().withNetworkAndContract( + this.getNetworkConfig().networkPassphrase as StellarNetworkId, + this.getChannelId() as ContractId, + ); + const accountHandler = new UtxoBasedStellarAccount({ + derivator, + root, + options: { + ...options, + fetchBalances: this.getBalancesFetcher(), + }, + }); + + return accountHandler; + } + //========================================== // Contract Read / Write Methods //========================================== diff --git a/src/privacy-channel/types.ts b/src/privacy-channel/types.ts index 51419a4..9eddd3c 100644 --- a/src/privacy-channel/types.ts +++ b/src/privacy-channel/types.ts @@ -1,6 +1,11 @@ -import type { ContractId, Ed25519PublicKey } from "@colibri/core"; +import type { + ContractId, + Ed25519PublicKey, + Ed25519SecretKey, +} from "@colibri/core"; import type { Buffer } from "node:buffer"; import type { ChannelInvokeMethods, ChannelReadMethods } from "./constants.ts"; +import { UTXOBasedAccountContructorArgs } from "../utxo-based-account/types.ts"; export type ChannelConstructorArgs = { admin: Ed25519PublicKey | ContractId; @@ -73,3 +78,19 @@ export type ChannelInvoke = { output: None; }; }; + +export type GetUTXOAccountHandlerArgs = + & Pick< + UTXOBasedAccountContructorArgs, + "root" + > + & { + options?: Omit< + UTXOBasedAccountContructorArgs< + string, + Ed25519SecretKey, + `${number}` + >["options"], + "fetchBalances" + >; + }; diff --git a/src/utxo-based-account/index.ts b/src/utxo-based-account/index.ts index dad9327..77ba804 100644 --- a/src/utxo-based-account/index.ts +++ b/src/utxo-based-account/index.ts @@ -2,7 +2,10 @@ import { UTXOKeypair } from "../core/utxo-keypair/index.ts"; import { UTXOStatus } from "../core/utxo-keypair/types.ts"; import type { BaseDerivator } from "../derivation/base/index.ts"; import { UTXOSelectionStrategy } from "./selection-strategy.ts"; -import type { UTXOSelectionResult } from "./types.ts"; +import type { + UTXOBasedAccountContructorArgs, + UTXOSelectionResult, +} from "./types.ts"; import * as E from "./error.ts"; import { assert } from "../utils/assert/assert.ts"; /** @@ -46,16 +49,7 @@ export class UtxoBasedAccount< /** * Creates a new UtxoBasedAccount instance */ - constructor(args: { - derivator: BaseDerivator; - root: Root; - options?: { - batchSize?: number; - fetchBalances?: (publicKeys: Uint8Array[]) => Promise; - startIndex?: number; - maxReservationAgeMs?: number; - }; - }) { + constructor(args: UTXOBasedAccountContructorArgs) { this.root = args.root; this.derivator = args.derivator; this.derivator.withRoot(this.root); diff --git a/src/utxo-based-account/types.ts b/src/utxo-based-account/types.ts index 7846198..fa4cf4d 100644 --- a/src/utxo-based-account/types.ts +++ b/src/utxo-based-account/types.ts @@ -1,4 +1,5 @@ import type { UTXOKeypair } from "../core/utxo-keypair/index.ts"; +import { BaseDerivator } from "../derivation/base/index.ts"; /** * Result of UTXO selection for transfers @@ -8,3 +9,18 @@ export interface UTXOSelectionResult { totalAmount: bigint; changeAmount: bigint; } + +export type UTXOBasedAccountContructorArgs< + Context extends string, + Root extends string, + Index extends `${number}`, +> = { + derivator: BaseDerivator; + root: Root; + options?: { + batchSize?: number; + fetchBalances?: (publicKeys: Uint8Array[]) => Promise; + startIndex?: number; + maxReservationAgeMs?: number; + }; +}; From eb492737eec24055c5b5c6e45c51030893a5e400 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 13 Nov 2025 09:11:36 -0300 Subject: [PATCH 81/90] fix: update return types for getBalancesFetcher, getTransactionBuilder, and getUTXOAccountHandler methods in PrivacyChannel; change import to type for UTXOBasedAccountContructorArgs --- src/privacy-channel/index.ts | 13 ++++++++----- src/privacy-channel/types.ts | 2 +- src/utxo-based-account/types.ts | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index 94bce65..4ba07dd 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -1,7 +1,6 @@ import { Contract, type ContractId, - Ed25519SecretKey, type NetworkConfig, type TransactionConfig, } from "@colibri/core"; @@ -154,9 +153,11 @@ export class PrivacyChannel { /** * Returns a function that fetches balances for given UTXO public keys. * - * @returns {(publicKeys: UTXOPublicKey[]) => Promise>} + * @returns {(publicKeys: Uint8Array[]) => Promise} */ - public getBalancesFetcher() { + public getBalancesFetcher(): ( + publicKeys: UTXOPublicKey[], + ) => Promise { const fetchBalances = (publicKeys: UTXOPublicKey[]) => { return this.read({ method: ChannelReadMethods.utxo_balances, @@ -173,7 +174,7 @@ export class PrivacyChannel { * * @returns {MoonlightTransactionBuilder} A pre-configured MoonlightTransactionBuilder instance. */ - public getTransactionBuilder() { + public getTransactionBuilder(): MoonlightTransactionBuilder { const txBuilder = new MoonlightTransactionBuilder({ channelId: this.getChannelId(), authId: this.getAuthId(), @@ -193,7 +194,9 @@ export class PrivacyChannel { * @param {Object} [args.options] - Additional options for the UTXO account handler. * @returns */ - public getUTXOAccountHandler(args: GetUTXOAccountHandlerArgs) { + public getUTXOAccountHandler( + args: GetUTXOAccountHandlerArgs, + ): UtxoBasedStellarAccount { const { root, options } = args; const derivator = new StellarDerivator().withNetworkAndContract( diff --git a/src/privacy-channel/types.ts b/src/privacy-channel/types.ts index 9eddd3c..46b2951 100644 --- a/src/privacy-channel/types.ts +++ b/src/privacy-channel/types.ts @@ -5,7 +5,7 @@ import type { } from "@colibri/core"; import type { Buffer } from "node:buffer"; import type { ChannelInvokeMethods, ChannelReadMethods } from "./constants.ts"; -import { UTXOBasedAccountContructorArgs } from "../utxo-based-account/types.ts"; +import type { UTXOBasedAccountContructorArgs } from "../utxo-based-account/types.ts"; export type ChannelConstructorArgs = { admin: Ed25519PublicKey | ContractId; diff --git a/src/utxo-based-account/types.ts b/src/utxo-based-account/types.ts index fa4cf4d..8cb866f 100644 --- a/src/utxo-based-account/types.ts +++ b/src/utxo-based-account/types.ts @@ -1,5 +1,5 @@ import type { UTXOKeypair } from "../core/utxo-keypair/index.ts"; -import { BaseDerivator } from "../derivation/base/index.ts"; +import type { BaseDerivator } from "../derivation/base/index.ts"; /** * Result of UTXO selection for transfers From ffcea80a3793340e9033a837b0778ae389fdf77b Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 13 Nov 2025 09:12:00 -0300 Subject: [PATCH 82/90] chore: update version number in deno.json from 0.4.0 to 0.5.0 --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 9965b5c..d411758 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@moonlight/moonlight-sdk", - "version": "0.4.0", + "version": "0.5.0", "description": "A privacy-focused toolkit for the Moonlight protocol on Stellar Soroban smart contracts.", "license": "MIT", "tasks": { From 0696dfad8809632ddcbd9aafb4158ae85054b026 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 13 Nov 2025 09:20:45 -0300 Subject: [PATCH 83/90] fix: correct spelling of UTXOBasedAccountConstructorArgs in types.ts and index.ts --- src/privacy-channel/types.ts | 6 +++--- src/utxo-based-account/index.ts | 4 ++-- src/utxo-based-account/types.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/privacy-channel/types.ts b/src/privacy-channel/types.ts index 46b2951..9a73e5f 100644 --- a/src/privacy-channel/types.ts +++ b/src/privacy-channel/types.ts @@ -5,7 +5,7 @@ import type { } from "@colibri/core"; import type { Buffer } from "node:buffer"; import type { ChannelInvokeMethods, ChannelReadMethods } from "./constants.ts"; -import type { UTXOBasedAccountContructorArgs } from "../utxo-based-account/types.ts"; +import type { UTXOBasedAccountConstructorArgs } from "../utxo-based-account/types.ts"; export type ChannelConstructorArgs = { admin: Ed25519PublicKey | ContractId; @@ -81,12 +81,12 @@ export type ChannelInvoke = { export type GetUTXOAccountHandlerArgs = & Pick< - UTXOBasedAccountContructorArgs, + UTXOBasedAccountConstructorArgs, "root" > & { options?: Omit< - UTXOBasedAccountContructorArgs< + UTXOBasedAccountConstructorArgs< string, Ed25519SecretKey, `${number}` diff --git a/src/utxo-based-account/index.ts b/src/utxo-based-account/index.ts index 77ba804..3c36c68 100644 --- a/src/utxo-based-account/index.ts +++ b/src/utxo-based-account/index.ts @@ -3,7 +3,7 @@ import { UTXOStatus } from "../core/utxo-keypair/types.ts"; import type { BaseDerivator } from "../derivation/base/index.ts"; import { UTXOSelectionStrategy } from "./selection-strategy.ts"; import type { - UTXOBasedAccountContructorArgs, + UTXOBasedAccountConstructorArgs, UTXOSelectionResult, } from "./types.ts"; import * as E from "./error.ts"; @@ -49,7 +49,7 @@ export class UtxoBasedAccount< /** * Creates a new UtxoBasedAccount instance */ - constructor(args: UTXOBasedAccountContructorArgs) { + constructor(args: UTXOBasedAccountConstructorArgs) { this.root = args.root; this.derivator = args.derivator; this.derivator.withRoot(this.root); diff --git a/src/utxo-based-account/types.ts b/src/utxo-based-account/types.ts index 8cb866f..bf74bb5 100644 --- a/src/utxo-based-account/types.ts +++ b/src/utxo-based-account/types.ts @@ -10,7 +10,7 @@ export interface UTXOSelectionResult { changeAmount: bigint; } -export type UTXOBasedAccountContructorArgs< +export type UTXOBasedAccountConstructorArgs< Context extends string, Root extends string, Index extends `${number}`, From dcdaac372487a7746409c5b3df8f05fcd868c165 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 13 Nov 2025 09:22:16 -0300 Subject: [PATCH 84/90] fix: update import path for UTXOPublicKey and enhance JSDoc for getUTXOAccountHandler method in PrivacyChannel --- src/privacy-channel/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index 4ba07dd..a8f4355 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -18,7 +18,7 @@ import type { } from "./types.ts"; import type { xdr } from "@stellar/stellar-sdk"; import * as E from "./error.ts"; -import type { UTXOPublicKey } from "@moonlight/moonlight-sdk"; +import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; import { Buffer } from "buffer"; import { MoonlightTransactionBuilder } from "../transaction-builder/index.ts"; import { UtxoBasedStellarAccount } from "../utxo-based-account/utxo-based-stellar-account/index.ts"; @@ -192,7 +192,7 @@ export class PrivacyChannel { * @param {GetUTXOAccountHandlerArgs} args - The arguments for creating the UTXO account handler. * @param {Ed25519SecretKey} args.root - The root secret key for the Stellar account. * @param {Object} [args.options] - Additional options for the UTXO account handler. - * @returns + * @returns {UtxoBasedStellarAccount} A handler for UTXO-based Stellar accounts, pre-configured for this privacy channel. Use this to manage UTXO-based operations for the associated Stellar account. */ public getUTXOAccountHandler( args: GetUTXOAccountHandlerArgs, From 7f75dde5f216ef25f70ea2339b52a300428c7014 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 13 Nov 2025 09:49:14 -0300 Subject: [PATCH 85/90] fix: change coverage status from patch to project in codecov.yml for accurate reporting --- codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecov.yml b/codecov.yml index 25f3570..4e6b330 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,5 +1,5 @@ coverage: status: - patch: + project: default: target: 70% From 5f4d710612aaf17062ebddfd3ca819c3d9176320 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 13 Nov 2025 09:49:36 -0300 Subject: [PATCH 86/90] fix: streamline PrivacyChannel and UtxoBasedStellarAccount integration - Removed unused properties and methods from PrivacyChannel, including _derivator and related methods. - Simplified getDerivator method to create a new StellarDerivator instance directly. - Added static methods in UtxoBasedStellarAccount and MoonlightTransactionBuilder to create instances from PrivacyChannel, enhancing integration. - Updated integration tests to utilize new static methods for creating transaction builders and UTXO account handlers, improving test clarity and maintainability. --- src/privacy-channel/index.ts | 79 +++---------------- src/privacy-channel/types.ts | 23 +----- src/transaction-builder/index.ts | 17 ++++ .../utxo-based-stellar-account/index.ts | 35 +++++++- .../privacy-channel.integration.test.ts | 13 +-- .../utxo-based-account.integration.test.ts | 27 +------ 6 files changed, 71 insertions(+), 123 deletions(-) diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index a8f4355..15c9298 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -11,24 +11,17 @@ import { ChannelReadMethods, ChannelSpec, } from "./constants.ts"; -import type { - ChannelInvoke, - ChannelRead, - GetUTXOAccountHandlerArgs, -} from "./types.ts"; +import type { ChannelInvoke, ChannelRead } from "./types.ts"; import type { xdr } from "@stellar/stellar-sdk"; import * as E from "./error.ts"; import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; import { Buffer } from "buffer"; -import { MoonlightTransactionBuilder } from "../transaction-builder/index.ts"; -import { UtxoBasedStellarAccount } from "../utxo-based-account/utxo-based-stellar-account/index.ts"; export class PrivacyChannel { private _client: Contract; private _authId: ContractId; private _assetId: ContractId; private _networkConfig: NetworkConfig; - private _derivator: StellarDerivator; public constructor( networkConfig: NetworkConfig, @@ -46,11 +39,6 @@ export class PrivacyChannel { this._authId = authId; this._assetId = assetId; - - this._derivator = new StellarDerivator().withNetworkAndContract( - networkConfig.networkPassphrase as StellarNetworkId, - channelId as ContractId, - ); } //========================================== @@ -69,11 +57,10 @@ export class PrivacyChannel { private require(arg: "_client"): Contract; private require(arg: "_authId"): ContractId; private require(arg: "_networkConfig"): NetworkConfig; - private require(arg: "_derivator"): StellarDerivator; private require(arg: "_assetId"): ContractId; private require( - arg: "_client" | "_authId" | "_networkConfig" | "_derivator" | "_assetId", - ): Contract | ContractId | NetworkConfig | StellarDerivator { + arg: "_client" | "_authId" | "_networkConfig" | "_assetId", + ): Contract | ContractId | NetworkConfig { if (this[arg]) return this[arg]; throw new E.PROPERTY_NOT_SET(arg); } @@ -133,10 +120,13 @@ export class PrivacyChannel { * * @params None * @returns {StellarDerivator} The StellarDerivator instance. - * @throws {Error} If the StellarDerivator instance is not set. + * @throws {Error} If the any of the underlying properties are not set. */ public getDerivator(): StellarDerivator { - return this.require("_derivator"); + return new StellarDerivator().withNetworkAndContract( + this.getNetworkConfig().networkPassphrase as StellarNetworkId, + this.getChannelId() as ContractId, + ); } /** @@ -158,8 +148,10 @@ export class PrivacyChannel { public getBalancesFetcher(): ( publicKeys: UTXOPublicKey[], ) => Promise { - const fetchBalances = (publicKeys: UTXOPublicKey[]) => { - return this.read({ + const fetchBalances = async ( + publicKeys: UTXOPublicKey[], + ): Promise => { + return await this.read({ method: ChannelReadMethods.utxo_balances, methodArgs: { utxos: publicKeys.map((pk) => Buffer.from(pk)) }, }); @@ -168,53 +160,6 @@ export class PrivacyChannel { return fetchBalances; } - /** - * Creates and returns a MoonlightTransactionBuilder instance - * pre-configured for this privacy channel. - * - * @returns {MoonlightTransactionBuilder} A pre-configured MoonlightTransactionBuilder instance. - */ - public getTransactionBuilder(): MoonlightTransactionBuilder { - const txBuilder = new MoonlightTransactionBuilder({ - channelId: this.getChannelId(), - authId: this.getAuthId(), - network: this.getNetworkConfig().networkPassphrase, - assetId: this.getAssetId(), - }); - - return txBuilder; - } - - /** - * Creates and returns a UtxoBasedStellarAccount handler - * pre-configured for this privacy channel. - * - * @param {GetUTXOAccountHandlerArgs} args - The arguments for creating the UTXO account handler. - * @param {Ed25519SecretKey} args.root - The root secret key for the Stellar account. - * @param {Object} [args.options] - Additional options for the UTXO account handler. - * @returns {UtxoBasedStellarAccount} A handler for UTXO-based Stellar accounts, pre-configured for this privacy channel. Use this to manage UTXO-based operations for the associated Stellar account. - */ - public getUTXOAccountHandler( - args: GetUTXOAccountHandlerArgs, - ): UtxoBasedStellarAccount { - const { root, options } = args; - - const derivator = new StellarDerivator().withNetworkAndContract( - this.getNetworkConfig().networkPassphrase as StellarNetworkId, - this.getChannelId() as ContractId, - ); - const accountHandler = new UtxoBasedStellarAccount({ - derivator, - root, - options: { - ...options, - fetchBalances: this.getBalancesFetcher(), - }, - }); - - return accountHandler; - } - //========================================== // Contract Read / Write Methods //========================================== diff --git a/src/privacy-channel/types.ts b/src/privacy-channel/types.ts index 9a73e5f..51419a4 100644 --- a/src/privacy-channel/types.ts +++ b/src/privacy-channel/types.ts @@ -1,11 +1,6 @@ -import type { - ContractId, - Ed25519PublicKey, - Ed25519SecretKey, -} from "@colibri/core"; +import type { ContractId, Ed25519PublicKey } from "@colibri/core"; import type { Buffer } from "node:buffer"; import type { ChannelInvokeMethods, ChannelReadMethods } from "./constants.ts"; -import type { UTXOBasedAccountConstructorArgs } from "../utxo-based-account/types.ts"; export type ChannelConstructorArgs = { admin: Ed25519PublicKey | ContractId; @@ -78,19 +73,3 @@ export type ChannelInvoke = { output: None; }; }; - -export type GetUTXOAccountHandlerArgs = - & Pick< - UTXOBasedAccountConstructorArgs, - "root" - > - & { - options?: Omit< - UTXOBasedAccountConstructorArgs< - string, - Ed25519SecretKey, - `${number}` - >["options"], - "fetchBalances" - >; - }; diff --git a/src/transaction-builder/index.ts b/src/transaction-builder/index.ts index 28d4147..38c7f5e 100644 --- a/src/transaction-builder/index.ts +++ b/src/transaction-builder/index.ts @@ -39,6 +39,7 @@ import type { import * as E from "./error.ts"; import { assert } from "../utils/assert/assert.ts"; import { assertExtOpsExist } from "./validators/operations.ts"; +import type { PrivacyChannel } from "../privacy-channel/index.ts"; export class MoonlightTransactionBuilder { private _create: CreateOperation[] = []; @@ -75,6 +76,22 @@ export class MoonlightTransactionBuilder { this._network = network; } + /** + * Creates a MoonlightTransactionBuilder instance from an existing PrivacyChannel. + * @param channelClient - The PrivacyChannel instance to use. + * @returns {MoonlightTransactionBuilder} A MoonlightTransactionBuilder instance pre-configured with the PrivacyChannel's parameters. + */ + static fromPrivacyChannel( + channelClient: PrivacyChannel, + ): MoonlightTransactionBuilder { + return new MoonlightTransactionBuilder({ + channelId: channelClient.getChannelId(), + authId: channelClient.getAuthId(), + network: channelClient.getNetworkConfig().networkPassphrase, + assetId: channelClient.getAssetId(), + }); + } + //========================================== // Meta Requirement Methods //========================================== diff --git a/src/utxo-based-account/utxo-based-stellar-account/index.ts b/src/utxo-based-account/utxo-based-stellar-account/index.ts index a9768cf..098b5f4 100644 --- a/src/utxo-based-account/utxo-based-stellar-account/index.ts +++ b/src/utxo-based-account/utxo-based-stellar-account/index.ts @@ -4,9 +4,42 @@ import type { StellarDerivationIndex, StellarDerivationRoot, } from "../../derivation/stellar/types.ts"; +import type { PrivacyChannel } from "../../privacy-channel/index.ts"; +import type { UTXOBasedAccountConstructorArgs } from "../types.ts"; export class UtxoBasedStellarAccount extends UtxoBasedAccount< StellarDerivationContext, StellarDerivationRoot, StellarDerivationIndex -> {} +> { + /** + * Create a UTXO-based Stellar account handler from a PrivacyChannel instance. + * + * @param args - The arguments for creating the UTXO-based Stellar account handler. + * @param args.channelClient - The PrivacyChannel instance to use. + * @param args.root - The root derivation key for the Stellar account. + * @param args.options - Additional options for the UTXO-based account handler. + * @returns {UtxoBasedStellarAccount} A UTXO-based Stellar account handler. + */ + static fromPrivacyChannel(args: { + channelClient: PrivacyChannel; + root: StellarDerivationRoot; + options?: Omit< + UTXOBasedAccountConstructorArgs< + StellarDerivationContext, + StellarDerivationRoot, + StellarDerivationIndex + >["options"], + "fetchBalances" + >; + }): UtxoBasedStellarAccount { + const { channelClient, root } = args; + return new UtxoBasedStellarAccount({ + derivator: channelClient.getDerivator(), + root, + options: { + fetchBalances: channelClient.getBalancesFetcher(), + }, + }); + } +} diff --git a/test/integration/privacy-channel.integration.test.ts b/test/integration/privacy-channel.integration.test.ts index 7d81dc3..21dd3d2 100644 --- a/test/integration/privacy-channel.integration.test.ts +++ b/test/integration/privacy-channel.integration.test.ts @@ -31,7 +31,6 @@ import { generateNonce, generateP256KeyPair, MoonlightOperation as op, - MoonlightTransactionBuilder, PrivacyChannel, } from "../../mod.ts"; @@ -39,6 +38,7 @@ import { Asset, Keypair } from "@stellar/stellar-sdk"; import { disableSanitizeConfig } from "../utils/disable-sanitize-config.ts"; import { Server } from "@stellar/stellar-sdk/rpc"; +import { MoonlightTransactionBuilder } from "../../src/transaction-builder/index.ts"; describe( "[Testnet - Integration] PrivacyChannel", @@ -232,14 +232,9 @@ describe( const utxoAKeypair = await generateP256KeyPair(); const utxoBKeypair = await generateP256KeyPair(); - const depositTx = new MoonlightTransactionBuilder({ - network: networkConfig.networkPassphrase, - channelId: channelId, - authId: authId, - assetId: Asset.native().contractId( - networkConfig.networkPassphrase, - ) as ContractId, - }); + const depositTx = MoonlightTransactionBuilder.fromPrivacyChannel( + channelClient, + ); const createOpA = op.create(utxoAKeypair.publicKey, 250n); const createOpB = op.create(utxoBKeypair.publicKey, 250n); diff --git a/test/integration/utxo-based-account.integration.test.ts b/test/integration/utxo-based-account.integration.test.ts index 05874dc..2065767 100644 --- a/test/integration/utxo-based-account.integration.test.ts +++ b/test/integration/utxo-based-account.integration.test.ts @@ -187,14 +187,7 @@ describe( root: testRoot, options: { batchSize: 10, - fetchBalances: async (publicKeys: Uint8Array[]) => { - return channelClient.read({ - method: ChannelReadMethods.utxo_balances, - methodArgs: { - utxos: publicKeys.map((pk) => Buffer.from(pk)), - }, - }); - }, + fetchBalances: channelClient.getBalancesFetcher(), }, }); @@ -254,25 +247,11 @@ describe( const testRoot = "S-TEST_SECRET_ROOT_3"; const depositAmount = 500000n; // 0.05 XLM - // Create a fresh derivator for this test - const freshDerivator = new StellarDerivator().withNetworkAndContract( - StellarNetworkId.Testnet, - channelId, - ); - - const utxoAccount = new UtxoBasedStellarAccount({ - derivator: freshDerivator, + const utxoAccount = UtxoBasedStellarAccount.fromPrivacyChannel({ + channelClient, root: testRoot, options: { batchSize: 10, - fetchBalances: async (publicKeys: Uint8Array[]) => { - return channelClient.read({ - method: ChannelReadMethods.utxo_balances, - methodArgs: { - utxos: publicKeys.map((pk) => Buffer.from(pk)), - }, - }); - }, }, }); From c4dc2db710360ddf0877746ae25fdd74197b1233 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 13 Nov 2025 09:58:26 -0300 Subject: [PATCH 87/90] fix: correct error message in getDerivator JSDoc for clarity --- src/privacy-channel/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/privacy-channel/index.ts b/src/privacy-channel/index.ts index 15c9298..322ff57 100644 --- a/src/privacy-channel/index.ts +++ b/src/privacy-channel/index.ts @@ -120,7 +120,7 @@ export class PrivacyChannel { * * @params None * @returns {StellarDerivator} The StellarDerivator instance. - * @throws {Error} If the any of the underlying properties are not set. + * @throws {Error} If any of the underlying properties are not set. */ public getDerivator(): StellarDerivator { return new StellarDerivator().withNetworkAndContract( From 48a9ca366b6756a53bb0f1bd891114bea645fd4b Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 13 Nov 2025 10:00:33 -0300 Subject: [PATCH 88/90] fix: ensure options are correctly merged in UtxoBasedStellarAccount.fromPrivacyChannel method --- src/utxo-based-account/utxo-based-stellar-account/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utxo-based-account/utxo-based-stellar-account/index.ts b/src/utxo-based-account/utxo-based-stellar-account/index.ts index 098b5f4..5d6e096 100644 --- a/src/utxo-based-account/utxo-based-stellar-account/index.ts +++ b/src/utxo-based-account/utxo-based-stellar-account/index.ts @@ -38,6 +38,7 @@ export class UtxoBasedStellarAccount extends UtxoBasedAccount< derivator: channelClient.getDerivator(), root, options: { + ...(args.options ?? {}), fetchBalances: channelClient.getBalancesFetcher(), }, }); From aec0d2bf1a94067f99868813e7dd432c0d9dc005 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Thu, 13 Nov 2025 10:23:37 -0300 Subject: [PATCH 89/90] fix: add informational status for patch coverage in codecov.yml --- codecov.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/codecov.yml b/codecov.yml index 4e6b330..de2fdc8 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,5 +1,8 @@ coverage: status: + patch: + default: + informational: true project: default: target: 70% From e290dae909d2d9de1379dd726e91f51c1c0e1020 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:08:45 -0300 Subject: [PATCH 90/90] feat: implement MLXDR operations bundle handling (#21) --- deno.json | 2 +- src/custom-xdr/index.ts | 65 ++++- src/custom-xdr/index.unit.test.ts | 261 +++++++++++++++++++ src/custom-xdr/types.ts | 4 +- src/operation/index.unit.test.ts | 4 +- src/transaction-builder/signing.unit.test.ts | 71 +++++ 6 files changed, 401 insertions(+), 6 deletions(-) create mode 100644 src/custom-xdr/index.unit.test.ts create mode 100644 src/transaction-builder/signing.unit.test.ts diff --git a/deno.json b/deno.json index d411758..518b345 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@moonlight/moonlight-sdk", - "version": "0.5.0", + "version": "0.6.0", "description": "A privacy-focused toolkit for the Moonlight protocol on Stellar Soroban smart contracts.", "license": "MIT", "tasks": { diff --git a/src/custom-xdr/index.ts b/src/custom-xdr/index.ts index 371ba6c..7682df2 100644 --- a/src/custom-xdr/index.ts +++ b/src/custom-xdr/index.ts @@ -16,7 +16,12 @@ import { Buffer } from "buffer"; import { MLXDRPrefix, MLXDRTypeByte } from "./types.ts"; import type { Condition as ConditionType } from "../conditions/types.ts"; -import { nativeToScVal, scValToBigInt, xdr } from "@stellar/stellar-sdk"; +import { + nativeToScVal, + scValToBigInt, + scValToNative, + xdr, +} from "@stellar/stellar-sdk"; import { Condition } from "../conditions/index.ts"; import { type MoonlightOperation as MoonlightOperationType, @@ -38,6 +43,8 @@ const MLXDROperationBytes = [ MLXDRTypeByte.WithdrawOperation, ]; +const MLXDROperationsBundleBytes = [MLXDRTypeByte.OperationsBundle]; + const MLXDRTransactionBundleBytes = [MLXDRTypeByte.TransactionBundle]; const isMLXDR = (data: string): boolean => { @@ -93,6 +100,13 @@ const isOperation = (data: string): boolean => { return MLXDROperationBytes.includes(prefixByte); }; +const isOperationsBundle = (data: string): boolean => { + const typePrefix = getMLXDRTypePrefix(data); + + const prefixByte = typePrefix[0]; + return MLXDROperationsBundleBytes.includes(prefixByte); +}; + const isTransactionBundle = (data: string): boolean => { const typePrefix = getMLXDRTypePrefix(data); @@ -310,6 +324,52 @@ const MLXDRtoOperation = (data: string): MoonlightOperationType => { } }; +const operationsBundleToMLXDR = ( + operations: MoonlightOperationType[], +): string => { + if (operations.length === 0) { + throw new Error("Operations bundle cannot be empty"); + } + const operationMLXDRArray = operations.map((op) => { + return xdr.ScVal.scvString(op.toMLXDR()); + }); + const typeByte: MLXDRTypeByte = MLXDRTypeByte.OperationsBundle; + + const operationBundleXDR = xdr.ScVal.scvVec([ + ...operationMLXDRArray, + ]).toXDR("base64"); + + return appendMLXDRPrefixToRawXDR(operationBundleXDR, typeByte); +}; + +const MLXDRtoOperationsBundle = (data: string): MoonlightOperationType[] => { + if (!isOperationsBundle(data)) { + throw new Error("Data is not a valid MLXDR Operations Bundle"); + } + + const buffer = Buffer.from(data, "base64"); + const rawXDRBuffer = buffer.slice(3); + const rawXDRString = rawXDRBuffer.toString("base64"); + + const scVal = xdr.ScVal.fromXDR(rawXDRString, "base64"); + + const vec = scVal.vec(); + + if (vec === null) { + throw new Error("Invalid ScVal vector for operations bundle"); + } + + const operations: MoonlightOperationType[] = vec.map((opScVal) => { + if (opScVal.switch().name !== xdr.ScValType.scvString().name) { + throw new Error("Invalid ScVal type for operation in bundle"); + } + const opMLXDR = scValToNative(opScVal) as string; + return MLXDRtoOperation(opMLXDR); + }); + + return operations; +}; + /** * * MLXDR Module * @@ -330,10 +390,13 @@ export const MLXDR = { is: isMLXDR, isCondition, isOperation, + isOperationsBundle, isTransactionBundle, getXDRType, fromCondition: conditionToMLXDR, toCondition: MLXDRtoCondition, fromOperation: operationToMLXDR, toOperation: MLXDRtoOperation, + fromOperationsBundle: operationsBundleToMLXDR, + toOperationsBundle: MLXDRtoOperationsBundle, }; diff --git a/src/custom-xdr/index.unit.test.ts b/src/custom-xdr/index.unit.test.ts new file mode 100644 index 0000000..89c4fdb --- /dev/null +++ b/src/custom-xdr/index.unit.test.ts @@ -0,0 +1,261 @@ +import { assert, assertEquals, assertExists } from "@std/assert"; +import { beforeAll, describe, it } from "@std/testing/bdd"; +import { + type ContractId, + type Ed25519PublicKey, + LocalSigner, +} from "@colibri/core"; +import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts"; +import { generateP256KeyPair } from "../utils/secp256r1/generateP256KeyPair.ts"; + +import { Asset, Networks } from "@stellar/stellar-sdk"; + +import { + type CreateOperation, + type DepositOperation, + type SpendOperation, + UTXOOperationType, + type WithdrawOperation, +} from "../operation/types.ts"; +import { MoonlightOperation } from "../operation/index.ts"; +import { UTXOKeypairBase } from "../core/utxo-keypair-base/index.ts"; +import { MLXDR } from "./index.ts"; + +describe("MLXDR", () => { + let validPublicKey: Ed25519PublicKey; + let validUtxo: UTXOPublicKey; + + let channelId: ContractId; + + let assetId: ContractId; + let network: string; + + beforeAll(async () => { + validPublicKey = LocalSigner.generateRandom() + .publicKey() as Ed25519PublicKey; + validUtxo = (await generateP256KeyPair()).publicKey as UTXOPublicKey; + + channelId = + "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC" as ContractId; + + network = Networks.TESTNET; + assetId = Asset.native().contractId(network) as ContractId; + }); + + describe("OperationsBundle MLXDR", () => { + it("should verify if the Operations Bundle is a valid MLXDR", () => { + const createOp = MoonlightOperation.create(validUtxo, 10n); + + const opBundleMLXDR = MLXDR.fromOperationsBundle([createOp]); + + assertEquals(MLXDR.isOperationsBundle(opBundleMLXDR), true); + }); + it("should convert to and from one operation", () => { + const createOp = MoonlightOperation.create(validUtxo, 10n); + + const opBundleMLXDR = MLXDR.fromOperationsBundle([createOp]); + + const recreatedOps = MLXDR.toOperationsBundle(opBundleMLXDR); + + assertEquals(recreatedOps.length, 1); + assertEquals(recreatedOps[0].getOperation(), UTXOOperationType.CREATE); + assertEquals(recreatedOps[0].getAmount(), 10n); + assert(recreatedOps[0].isCreate()); + + assertEquals( + (recreatedOps[0] as CreateOperation).getUtxo().toString(), + validUtxo.toString(), + ); + }); + + it("should convert to and from multiple Operations", () => { + const createOpA = MoonlightOperation.create(validUtxo, 10n); + const createOpB = MoonlightOperation.create(validUtxo, 20n); + const createOpC = MoonlightOperation.create(validUtxo, 30n); + + const opBundleMLXDR = MLXDR.fromOperationsBundle([ + createOpA, + createOpB, + createOpC, + ]); + + const recreatedOps = MLXDR.toOperationsBundle(opBundleMLXDR); + + assertEquals(recreatedOps.length, 3); + + assertEquals(recreatedOps[0].getOperation(), UTXOOperationType.CREATE); + assertEquals(recreatedOps[0].getAmount(), 10n); + assert(recreatedOps[0].isCreate()); + assertEquals( + (recreatedOps[0] as CreateOperation).getUtxo().toString(), + validUtxo.toString(), + ); + + assertEquals(recreatedOps[1].getOperation(), UTXOOperationType.CREATE); + assertEquals(recreatedOps[1].getAmount(), 20n); + assert(recreatedOps[1].isCreate()); + assertEquals( + (recreatedOps[1] as CreateOperation).getUtxo().toString(), + validUtxo.toString(), + ); + + assertEquals(recreatedOps[2].getOperation(), UTXOOperationType.CREATE); + assertEquals(recreatedOps[2].getAmount(), 30n); + assert(recreatedOps[2].isCreate()); + assertEquals( + (recreatedOps[2] as CreateOperation).getUtxo().toString(), + validUtxo.toString(), + ); + }); + + it("should convert to and from multiple mixed operations", () => { + const createOp = MoonlightOperation.create(validUtxo, 10n); + const depositOp = MoonlightOperation.deposit(validPublicKey, 20n); + const withdrawOp = MoonlightOperation.withdraw(validPublicKey, 30n); + const spendOp = MoonlightOperation.spend(validUtxo); + + const opBundleMLXDR = MLXDR.fromOperationsBundle([ + createOp, + depositOp, + withdrawOp, + spendOp, + ]); + + const recreatedOps = MLXDR.toOperationsBundle(opBundleMLXDR); + + assertEquals(recreatedOps.length, 4); + + assertEquals(recreatedOps[0].getOperation(), UTXOOperationType.CREATE); + assertEquals(recreatedOps[0].getAmount(), 10n); + assert(recreatedOps[0].isCreate()); + assertEquals( + (recreatedOps[0] as CreateOperation).getUtxo().toString(), + validUtxo.toString(), + ); + + assertEquals(recreatedOps[1].getOperation(), UTXOOperationType.DEPOSIT); + assertEquals(recreatedOps[1].getAmount(), 20n); + assert(recreatedOps[1].isDeposit()); + assertEquals( + (recreatedOps[1] as DepositOperation).getPublicKey().toString(), + validPublicKey.toString(), + ); + + assertEquals(recreatedOps[2].getOperation(), UTXOOperationType.WITHDRAW); + assertEquals(recreatedOps[2].getAmount(), 30n); + assert(recreatedOps[2].isWithdraw()); + assertEquals( + (recreatedOps[2] as WithdrawOperation).getPublicKey().toString(), + validPublicKey.toString(), + ); + + assertEquals(recreatedOps[3].getOperation(), UTXOOperationType.SPEND); + assert(recreatedOps[3].isSpend()); + assertEquals( + (recreatedOps[3] as SpendOperation).getUtxo().toString(), + validUtxo.toString(), + ); + }); + + it("should handle signed operations in an Operation Bundle", async () => { + const userSigner = LocalSigner.generateRandom(); + const spender = new UTXOKeypairBase(await generateP256KeyPair()); + const createOp = MoonlightOperation.create(validUtxo, 1000n); + + const depositOp = await MoonlightOperation.deposit( + userSigner.publicKey() as Ed25519PublicKey, + 1000n, + ).addCondition(createOp.toCondition()).signWithEd25519( + userSigner, + 100000, + channelId, + assetId, + network, + ); + + const spendOp = await MoonlightOperation.spend( + spender.publicKey, + ).addCondition(createOp.toCondition()).signWithUTXO( + spender, + channelId, + 1000, + ); + + const withdrawOp = await MoonlightOperation.withdraw( + userSigner.publicKey() as Ed25519PublicKey, + 500n, + ).addCondition(createOp.toCondition()); + + const opBundleMLXDR = MLXDR.fromOperationsBundle([ + createOp, + depositOp, + withdrawOp, + spendOp, + ]); + + const recreatedOps = MLXDR.toOperationsBundle(opBundleMLXDR); + + assertEquals(recreatedOps.length, 4); + + // Validate create Operation Signature + assertEquals(recreatedOps[0].getOperation(), UTXOOperationType.CREATE); + assertEquals(recreatedOps[0].getAmount(), 1000n); + assert(recreatedOps[0].isCreate()); + assertEquals( + (recreatedOps[0] as CreateOperation).getUtxo().toString(), + validUtxo.toString(), + ); + + // Validate deposit Operation Signature + assertEquals(recreatedOps[1].getOperation(), UTXOOperationType.DEPOSIT); + assertEquals(recreatedOps[1].getAmount(), 1000n); + assert(recreatedOps[1].isDeposit()); + assertEquals( + (recreatedOps[1] as DepositOperation).getPublicKey().toString(), + userSigner.publicKey().toString(), + ); + assertEquals( + (recreatedOps[1] as DepositOperation).isSignedByEd25519(), + true, + ); + assertExists( + (recreatedOps[1] as DepositOperation).getEd25519Signature(), + ); + assertEquals( + (recreatedOps[1] as DepositOperation).getConditions(), + depositOp.getConditions(), + ); + // Validate withdraw Operation Signature + assertEquals(recreatedOps[2].getOperation(), UTXOOperationType.WITHDRAW); + assertEquals(recreatedOps[2].getAmount(), 500n); + assert(recreatedOps[2].isWithdraw()); + assertEquals( + (recreatedOps[2] as WithdrawOperation).getPublicKey().toString(), + userSigner.publicKey().toString(), + ); + + assertEquals( + (recreatedOps[2] as WithdrawOperation).getConditions(), + withdrawOp.getConditions(), + ); + // Validate spend Operation Signature + assertEquals(recreatedOps[3].getOperation(), UTXOOperationType.SPEND); + assert(recreatedOps[3].isSpend()); + assertEquals( + (recreatedOps[3] as SpendOperation).getUtxo().toString(), + spender.publicKey.toString(), + ); + assertEquals( + (recreatedOps[3] as SpendOperation).isSignedByUTXO(), + true, + ); + assertExists( + (recreatedOps[3] as SpendOperation).getUTXOSignature(), + ); + assertEquals( + (recreatedOps[3] as SpendOperation).getConditions(), + spendOp.getConditions(), + ); + }); + }); +}); diff --git a/src/custom-xdr/types.ts b/src/custom-xdr/types.ts index 5d9862a..4b23298 100644 --- a/src/custom-xdr/types.ts +++ b/src/custom-xdr/types.ts @@ -8,8 +8,8 @@ export enum MLXDRTypeByte { SpendOperation = 0x05, DepositOperation = 0x06, WithdrawOperation = 0x07, - - TransactionBundle = 0x08, + OperationsBundle = 0x08, + TransactionBundle = 0x09, } export const MLXDRPrefix: Buffer = Buffer.from([0x30, 0xb0]); diff --git a/src/operation/index.unit.test.ts b/src/operation/index.unit.test.ts index 7cf5f9a..62ca04e 100644 --- a/src/operation/index.unit.test.ts +++ b/src/operation/index.unit.test.ts @@ -12,9 +12,9 @@ import { UTXOOperationType } from "./types.ts"; import type { CreateCondition } from "../conditions/types.ts"; import { Condition } from "../conditions/index.ts"; import { Asset, Networks } from "@stellar/stellar-sdk"; -import { UTXOKeypairBase } from "@moonlight/moonlight-sdk"; +import { UTXOKeypairBase } from "../core/utxo-keypair-base/index.ts"; -describe("Condition", () => { +describe("Operation", () => { let validPublicKey: Ed25519PublicKey; let validUtxo: UTXOPublicKey; diff --git a/src/transaction-builder/signing.unit.test.ts b/src/transaction-builder/signing.unit.test.ts new file mode 100644 index 0000000..2b8960e --- /dev/null +++ b/src/transaction-builder/signing.unit.test.ts @@ -0,0 +1,71 @@ +import { assertExists } from "@std/assert/exists"; +import { assertEquals } from "@std/assert/equals"; +import { MoonlightTransactionBuilder } from "./index.ts"; +import { beforeAll, describe, it } from "@std/testing/bdd"; +import { type ContractId, LocalSigner } from "@colibri/core"; +import { Asset, Networks } from "@stellar/stellar-sdk"; +import { Condition } from "../conditions/index.ts"; +import { MoonlightOperation } from "../operation/index.ts"; + +describe("MoonlightTransactionBuilder", () => { + let validAmount: bigint; + let channelId: ContractId; + let authId: ContractId; + let assetId: ContractId; + let network: string; + + beforeAll(() => { + validAmount = 1000n; + channelId = + "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC" as ContractId; + authId = + "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA" as ContractId; + network = Networks.TESTNET; + assetId = Asset.native().contractId(network) as ContractId; + }); + + describe("Signing", () => { + it("should create the same signature for individual operations and bundle", async () => { + const userSigner = LocalSigner.generateRandom(); + + const condition = Condition.deposit(userSigner.publicKey(), validAmount); + const rawOperation = MoonlightOperation.deposit( + userSigner.publicKey(), + validAmount, + ) + .addCondition(condition); + const signedOperation = await MoonlightOperation.deposit( + userSigner.publicKey(), + validAmount, + ) + .addCondition(condition).signWithEd25519( + userSigner, + 100000, + channelId, + assetId, + network, + ); + const testBuilder = new MoonlightTransactionBuilder({ + channelId, + authId, + assetId, + network, + }); + + testBuilder.addOperation(rawOperation); + + await testBuilder.signExtWithEd25519( + userSigner, + 100000, + ); + + const signedOpFromBundle = testBuilder.getDepositOperations()[0]; + + assertExists(signedOpFromBundle); + assertEquals( + signedOpFromBundle.getEd25519Signature().toString(), + signedOperation.getEd25519Signature().toString(), + ); + }); + }); +});