Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 28 additions & 10 deletions packages/core/src/actions/wallet/executeContract.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { AccountNotFoundError, ProvingNotConfiguredError } from '../../errors/errors.js'
import type { Client } from '../../clients/createClient.js'
import type { RawExecuteResult } from '../../types/proving.js'
import { waitForConfirmation } from '../../utils/waitForConfirmation.js'
import { extractTransitions } from '../../utils/extractTransitions.js'

export type ExecuteContractParameters = {
program: string
Expand All @@ -17,13 +19,20 @@ export type ExecuteContractReturnType = RawExecuteResult

/**
* Executes a program function end-to-end: build, prove, broadcast, wait for
* confirmation, and return raw output strings.
* confirmation, and return per-transition outputs.
*
* Behavior by account type:
* - Local account: proves (locally or via DPS), broadcasts, waits, returns outputs
* - RPC account: delegates entire flow to the connected wallet
* - Local account: proves (locally or via DPS), broadcasts, waits, decrypts owned record
* outputs with the self-custodied view key, returns outputs.
* - RPC account: wallet submits and proves; the SDK then polls the chain for confirmation
* and walks the transitions itself. The SDK does NOT ask the wallet to decrypt — that's
* a permission boundary the dApp shouldn't cross. Record outputs surface as raw
* `record1...` ciphertexts; plaintext outputs surface verbatim.
*
* The RPC path requires a transport that can reach the chain (e.g. an HTTP transport,
* or a fallback that includes one). A wallet-only transport will time out on the
* confirmation poll.
*
* Throws if execute is not configured on the proving config.
* Use simulateContract for local-only execution without broadcasting.
*/
export async function executeContract(
Expand All @@ -36,18 +45,27 @@ export async function executeContract(
}

if (account.type === 'rpc') {
// RPC account — wallet handles everything
return client.request({
// 1. Submit via wallet — adapter transport returns the tx id. Param shape mirrors
// writeContract: the wallet handles fee + program-source resolution internally, so
// `fee` and `programSource` on this action don't translate to the wire call.
const txId = await client.request({
method: 'executeTransaction',
params: {
programName: params.program,
functionName: params.function,
inputs: params.inputs,
fee: params.fee,
programSource: params.programSource,
imports: params.imports,
privateFee: params.privateFee,
imports: params.imports ? Object.keys(params.imports) : undefined,
},
}) as Promise<ExecuteContractReturnType>
}) as string

// 2. Wait for chain confirmation via the same transport.
const confirmedTx = await waitForConfirmation(client, txId)

// 3. Walk transitions; no decryptor (see docstring).
const { transitions, outputs } = extractTransitions(confirmedTx)

return { transactionId: txId, transitions, outputs }
}

if (account.type === 'local') {
Expand Down
9 changes: 6 additions & 3 deletions packages/core/src/clients/decorators/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ import type { Client } from '../createClient.js'
export type WalletActions = {
sendTransaction: (params: SendTransactionParameters) => Promise<SendTransactionReturnType>
writeContract: (params: WriteContractParameters) => Promise<WriteContractReturnType>
/** Alias for writeContract — matches the Aleo wallet adapter spec (submit and return a transaction id). */
executeTransaction: (params: WriteContractParameters) => Promise<WriteContractReturnType>
/** Execute locally and return outputs without broadcasting (local accounts only) */
simulateContract: (params: SimulateContractParameters) => Promise<SimulateContractReturnType>
/** Build, broadcast, wait for confirmation, and return outputs. Matches wallet adapter standard naming. */
executeTransaction: (params: ExecuteContractParameters) => Promise<ExecuteContractReturnType>
/** Build, broadcast, wait for confirmation, and return per-transition outputs. */
executeContract: (params: ExecuteContractParameters) => Promise<ExecuteContractReturnType>
deployContract: (params: DeployContractParameters) => Promise<DeployContractReturnType>
signMessage: (params: SignMessageParameters) => Promise<SignMessageReturnType>
transfer: (params: TransferParameters) => Promise<TransferReturnType>
Expand All @@ -39,8 +41,9 @@ export function walletActions(client: Client): WalletActions {
return {
sendTransaction: (params) => sendTransaction(client, params),
writeContract: (params) => writeContract(client, params),
executeTransaction: (params) => writeContract(client, params),
simulateContract: (params) => simulateContract(client, params),
executeTransaction: (params) => executeContract(client, params),
executeContract: (params) => executeContract(client, params),
deployContract: (params) => deployContract(client, params),
signMessage: (params) => signMessage(client, params),
transfer: (params) => transfer(client, params),
Expand Down
11 changes: 5 additions & 6 deletions packages/core/src/contract/getContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,28 +256,27 @@ export function getContract(params: GetContractParameters): ContractInstance {
}
return async (execParams: ContractExecuteParams) => {
validateFunction(prop)
const result = await walletClient.executeTransaction({
const result = await walletClient.executeContract({
program,
function: prop,
inputs: resolveInputs(execParams.inputs, prop),
programSource,
imports: { ...contractImports, ...execParams.imports },
})

// Build per-transition parsed results
// Build per-transition parsed results.
// Same-program transitions: parse with local ABI. Foreign: loose parse.
const transitions: ContractTransitionResult[] = (result.transitions ?? []).map(t => ({
transitionId: t.transitionId,
program: t.program,
function: t.function,
// Same-program transitions: parse with local ABI. Foreign: loose parse.
outputs: t.program === program
? parseOutputs(t.outputs, t.function)
: t.outputs.map(o => parseLooseOutput(o)),
}))

// Top-level outputs: the transition matching the called program/function
const topLevel = transitions.find(t => t.program === program && t.function === prop)
const outputs = topLevel?.outputs ?? parseOutputs(result.outputs, prop)
// Raw `outputs` is already the called function's transition outputs (set by extractTransitions).
const outputs = parseOutputs(result.outputs, prop)

return { transactionId: result.transactionId, transitions, outputs }
}
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ export { parseValue, encodeValue, type ParsedValue } from './utils/values.js'
export { parseRecordPlaintext, parseRecordPlaintextLoose, toString, serializeRecord, encodeInputs, getRecordDef, getInputTypes } from './utils/records.js'
export { parsePrimitive, parsePlaintext } from './utils/parsePrimitives.js'
export { parseAbi } from './utils/parseAbi.js'
export { waitForConfirmation } from './utils/waitForConfirmation.js'
export { extractTransitions, type Decryptor } from './utils/extractTransitions.js'

// Transports
export { createTransport } from './transports/createTransport.js'
Expand Down Expand Up @@ -204,8 +206,8 @@ export { readMapping } from './actions/public/readMapping.js'

// Wallet Actions (standalone)
export { simulateContract, type SimulateContractParameters, type SimulateContractReturnType } from './actions/wallet/simulateContract.js'
export { executeContract as executeTransaction, type ExecuteContractParameters, type ExecuteContractReturnType } from './actions/wallet/executeContract.js'
export { writeContract } from './actions/wallet/writeContract.js'
export { executeContract, type ExecuteContractParameters, type ExecuteContractReturnType } from './actions/wallet/executeContract.js'
export { writeContract, executeTransaction, type WriteContractParameters, type WriteContractReturnType } from './actions/wallet/writeContract.js'
export { deployContract } from './actions/wallet/deployContract.js'
export { sendTransaction } from './actions/wallet/sendTransaction.js'
export { signMessage } from './actions/wallet/signMessage.js'
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/types/proving.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export type RawExecuteResult = {
transactionId: string
/** Per-transition results with program/function metadata */
transitions: RawTransitionResult[]
/** Flat projection of all transition outputs for backwards compatibility */
/** Outputs of the called function's transition only — inner cross-program transition outputs live in `transitions[]`. */
outputs: string[]
}

Expand Down
61 changes: 61 additions & 0 deletions packages/core/src/utils/extractTransitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { RawTransitionResult } from '../types/proving.js'

/**
* Decryption strategy for a single record ciphertext.
*
* Returns the plaintext string if the caller can decrypt it, or `null` if not
* (e.g. not owned, no view key available). The {@link extractTransitions}
* function calls this for each record output and drops the entry when `null`
* is returned — matching the convention that unowned records do not appear in
* the caller's outputs.
*
* When no decryptor is provided, record ciphertexts pass through as raw
* `record1...` strings. This is the right shape for RPC accounts, where the
* dApp should not be brokering decryption on behalf of the wallet.
*/
export type Decryptor = (ciphertext: string) => string | null

/**
* Walk a confirmed transaction's `execution.transitions[]`, optionally decrypt
* record ciphertexts, and return the structured per-transition outputs plus
* the top-level transition's outputs.
*
* Aleo execution semantics guarantee that the outer/called transition is the
* last element of `execution.transitions[]` — inner cross-program transitions
* are recorded before their callers. So `outputs` is derived from
* `transitions.at(-1)`, with no name matching needed.
*/
export function extractTransitions(
tx: any,
decrypt?: Decryptor,
): { transitions: RawTransitionResult[]; outputs: string[] } {
const rawTransitions: RawTransitionResult[] = []

for (const transition of tx.execution?.transitions ?? []) {
const transitionOutputs: string[] = []
for (const output of transition.outputs ?? []) {
if (!output.value) continue
const isRecordCiphertext =
(output.type === 'record' || output.type === 'record_with_dynamic_id') &&
typeof output.value === 'string' &&
output.value.startsWith('record1')

if (isRecordCiphertext && decrypt) {
const plaintext = decrypt(output.value)
if (plaintext !== null) transitionOutputs.push(plaintext)
// else: not owned — drop (matches local-path semantics)
} else {
// Not a record ciphertext, OR no decryptor (RPC path) — pass through verbatim.
transitionOutputs.push(output.value)
}
}
rawTransitions.push({
transitionId: transition.id ?? '',
program: transition.program ?? '',
function: transition.function ?? '',
outputs: transitionOutputs,
})
}

return { transitions: rawTransitions, outputs: rawTransitions.at(-1)?.outputs ?? [] }
}
44 changes: 44 additions & 0 deletions packages/core/src/utils/waitForConfirmation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Client } from '../clients/createClient.js'
import type { ConfirmedTransaction } from '../types/block.js'
import { FinalizeRevertError, TransactionTimeoutError } from '../errors/errors.js'

const DEFAULT_TIMEOUT_MS = 300_000
const POLL_INTERVAL_MS = 5_000

/**
* Poll the chain until `txId` is confirmed, then return the inner transaction object.
*
* Goes through `client.request({ method: 'getConfirmedTransaction', ... })`, so the
* caller's transport must be able to reach the chain (HTTP transport or a fallback
* that includes one). Wallet-only transports will fail every poll and time out.
*
* Throws `FinalizeRevertError` if the confirmation envelope reports `status: 'rejected'`.
* Throws `TransactionTimeoutError` if no confirmation arrives within `timeoutMs`.
*/
export async function waitForConfirmation(
client: Client,
txId: string,
timeoutMs: number = DEFAULT_TIMEOUT_MS,
): Promise<Record<string, unknown>> {
const startTime = Date.now()
let lastError: unknown
while (Date.now() - startTime < timeoutMs) {
try {
const confirmed = await client.request({
method: 'getConfirmedTransaction',
params: { id: txId },
}) as ConfirmedTransaction | null
if (confirmed) {
if (confirmed.status === 'rejected') {
throw new FinalizeRevertError(txId)
}
return confirmed.transaction
}
} catch (e) {
if (e instanceof FinalizeRevertError) throw e
lastError = e
}
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS))
}
throw new TransactionTimeoutError({ transactionId: txId, timeoutMs, cause: lastError as Error | undefined })
}
Loading