Skip to content
Closed
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
2 changes: 2 additions & 0 deletions packages/sdk/scripts/mainnet-micro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
runMainnetReadiness,
runMicroRunnerGate,
} from "../src/mainnet-readiness.js";
import { assertMainnetConfirmed, assertReadinessForExecute } from "../src/mainnet-readiness.js";
import { generateAuditorKeypair, generateNonce, quicknet, sealBid } from "@sub-rosa/tlock";
import { systemClock } from "@sub-rosa/time";

Expand Down Expand Up @@ -173,6 +174,7 @@ async function main() {
value: bid,
nonce,
round: revealRound,
revealRound: revealRound,
client: drand,
identity: new TextEncoder().encode(`micro:${bidderKp.publicKey()}`),
auditorPublicKey: auditor.publicKey,
Expand Down
4 changes: 3 additions & 1 deletion packages/sdk/src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { Keypair, rpc, StrKey } from "@stellar/stellar-sdk";
import { readFileSync } from "node:fs";

import { SubRosaClient } from "./client.js";
import {
SubRosaClientConfigError,
SubRosaNetworkMismatchError,
SubRosaSessionMismatchError,
SubRosaSubmitError,
} from "./errors.js";
SubRosaPaginationError,
} from "./errors";
import type {
SubmitSignedTransactionParams,
TransactionSubmitter,
Expand Down
26 changes: 20 additions & 6 deletions packages/sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
Result,
} from "@stellar/stellar-sdk/contract";
import type { RoundAssetConfig } from "@sub-rosa/round-bindings";
/** Local SDK RoundAssetConfig shape - mirrors the Rust struct until bindings are regenerated. */
import { basicNodeSigner } from "@stellar/stellar-sdk/contract";
import {
Client as RoundContract,
Expand All @@ -28,7 +29,9 @@ import {
expectedRoundEventSequence,
} from "@sub-rosa/round-bindings/event-snapshot";
import { toHex } from "@sub-rosa/tlock";
import type { RoundReceiptEvent } from "./receipt.js";
import type { SealedBid } from "@sub-rosa/tlock";
import { SubRosaPaginationError } from "./errors.js";
import type { RoundReceipt } from "./receipt.js";
import { assertSealedBid } from "./encrypted-blob.js";
import type { SealedBidBinding } from "./encrypted-blob.js";
Expand Down Expand Up @@ -118,6 +121,11 @@ export interface SubRosaClientConfig {
* If not provided, no asset validation is performed.
*/
assetConfig?: import("./asset-config.js").AssetConfig;
/**
* Optional passkey session binding for the client.
* When provided, commits will validate the session before proceeding.
*/
session?: PasskeySessionBinding;
/**
* @internal Testing hook: inject a mock Soroban RPC server for simulation.
*/
Expand All @@ -127,14 +135,16 @@ export interface SubRosaClientConfig {
export type ClearingRuleTag = ClearingRule["tag"];

/**
* The contract's `RoundAssetConfig` (contracts/round/src/types.rs).
*
* The contract's `RoundAssetConfig` (contracts/round/src/types.rs). Local SDK shape in `SdkRoundAssetConfig`. *
* Declared here rather than imported because the generated bindings in this
* tree predate the `asset_config` argument on `create_round`; the shape mirrors
* the Rust struct exactly. Once the bindings are regenerated this type and the
* accompanying cast at the call site can both be dropped.
*/
interface RoundAssetConfig {
/** The contract's RoundAssetConfig shape, mirroring contracts/round/src/types.rs.
* Declared locally because the generated bindings predate the asset_config argument.
* Will be removable once the bindings are regenerated. */
interface SdkRoundAssetConfig {
asset_type: string;
contract_id: string;
code: string;
Expand Down Expand Up @@ -165,6 +175,10 @@ export interface CommitParams {
roundId: number | bigint;
/** The off-chain seal produced by @sub-rosa/tlock `sealBid`. */
sealed: SealedBid;
/** Binding used to verify the sealed bid's identity. */
binding?: SealedBidBinding;
/** Optional passkey session binding for session validation. */
session?: PasskeySessionBinding;
/** Public USDC budget locked now; upper bound on the sealed bid. */
escrow: bigint;
/** Bidder address. Default: the configured signer's public key. */
Expand Down Expand Up @@ -428,7 +442,7 @@ export class SubRosaClient {
#sleep: (ms: number) => Promise<void> = (ms) => this.#scheduler.sleep(ms);

// ── State-changing calls (sign + submit over RPC) ────────────────────── /** Build the on-chain asset_config argument from SDK params. */
#buildAssetConfig(params: CreateRoundParams): RoundAssetConfig {
#buildAssetConfig(params: CreateRoundParams): SdkRoundAssetConfig {
if (!params.assetConfig) {
return {
asset_type: "native",
Expand All @@ -455,7 +469,7 @@ export class SubRosaClient {
values: undefined,
} as ClearingRule;
// Build asset config for the round
let assetConfig: RoundAssetConfig = {
let assetConfig: SdkRoundAssetConfig = {
asset_type: "native",
contract_id: "",
code: "XLM",
Expand Down Expand Up @@ -777,7 +791,7 @@ export class SubRosaClient {
return proveEscrowConservationFromPages(
{
getBiddersPage: async (cursor, limit) =>
(await this.getBiddersPage(rid, cursor, limit)) as BiddersPage,
(await this.getBiddersPage(rid, cursor === undefined ? undefined : (cursor as unknown as Uint8Array), limit)) as BiddersPage,
getBidState: async (bidder) => {
try {
return await this.getBidState(rid, bidder);
Expand Down
7 changes: 2 additions & 5 deletions packages/sdk/src/conservation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,8 @@ function source(options: FakeIndex): EscrowConservationSource & { calls: number
const data = options.pages ? options.pages(slice) : slice;
return {
data,
next_cursor: options.nextCursor
? options.nextCursor(cursor)
: cursor + size >= options.bidders.length
? 0
: cursor + size,
has_more: true,
next_cursor: undefined,
total: options.totals ? options.totals(cursor) : options.bidders.length,
};
},
Expand Down
29 changes: 16 additions & 13 deletions packages/sdk/src/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
SubRosaMissingReturnValueError,
SubRosaNetworkMismatchError,
SubRosaTimeoutError,
} from "./errors.js";
} from "./errors";
import { Errors as RoundBindingsErrors } from "@sub-rosa/round-bindings";
import { SubRosaClient } from "./client.js";
import { createFakeTime } from "@sub-rosa/time";
Expand Down Expand Up @@ -377,23 +377,26 @@ describe("ROUND_CONTRACT_ERRORS mapping coverage", () => {

it("fails when a fixture error without a mapping is present", () => {
const fixtureWithUnmapped = [
...Object.values(ROUND_CONTRACT_ERRORS).map((e) => ({
code: e.code,
name: e.name,
...Object.entries(ROUND_CONTRACT_ERRORS).map(([, code]) => ({
code,
name: String(code),
})),
{ code: 99, name: "UnmappedContractError" },
];
{ code: 99, name: "UnmappedContractError" }];
const failures = diffContractErrorMapping(fixtureWithUnmapped);
assert.equal(failures.length, 1);
assert.match(failures[0], /UnmappedContractError/);
assert.match(failures[0], /not mapped/);
});

it("fails when a mapped SDK error is missing from the contract list", () => {
const incompleteList = Object.values(ROUND_CONTRACT_ERRORS)
.filter((e) => e.name !== "NotInitialized")
.map((e) => ({ code: e.code, name: e.name }));
const incompleteList = Object.entries(ROUND_CONTRACT_ERRORS)
.filter(([, name]: [string, number]) => {
const n = name as unknown as string;
return n !== "NotInitialized";
})
.map(([, v]: [string, number]) => ({ code: v, name: String(v) }));
const failures = diffContractErrorMapping(incompleteList);

assert.equal(failures.length, 1);
assert.match(failures[0], /NotInitialized/);
assert.match(failures[0], /missing from contract errors list/);
Expand Down Expand Up @@ -480,12 +483,12 @@ describe("isRoundContractErrorRetryable classification", () => {

it("treats unknown errors as non-retryable", () => {
assert.equal(isRoundContractErrorRetryable("UnknownTrap"), false);
assert.equal(isRoundContractErrorRetryable(999), false);
assert.equal(isRoundContractErrorRetryable(0), false);
assert.equal(isRoundContractErrorRetryable(999 as number), false);
assert.equal(isRoundContractErrorRetryable(0 as number), false);
assert.equal(isRoundContractErrorRetryable(undefined), false);
assert.equal(isRoundContractErrorRetryable(null), false);
assert.equal(isRoundContractErrorRetryable(null as null), false);
assert.equal(getRoundContractError("UnknownTrap"), undefined);
assert.equal(getRoundContractError(999), undefined);
assert.equal(getRoundContractError(String(999)), undefined);
});
});

Expand Down
Loading