From b358a736809447662a0b0578249015b090bda1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Crhemy-arc=E2=80=9D?= <“rhemaadzer@gmail.com”> Date: Tue, 21 Jul 2026 19:56:31 +0100 Subject: [PATCH 01/11] feat(codegen): add Soroban contract spec format and codegen script Add JSON-based contract spec format for describing Soroban contract interfaces (methods, args, return types, events, custom types). Add codegen script that reads specs and generates TypeScript bindings with compile-time checked contract calls and typed event data. --- scripts/generate-contract-bindings.js | 245 +++++++++++++++++++++++++ shared/contracts-raw/dispute.spec.json | 103 +++++++++++ shared/contracts-raw/escrow.spec.json | 93 ++++++++++ 3 files changed, 441 insertions(+) create mode 100644 scripts/generate-contract-bindings.js create mode 100644 shared/contracts-raw/dispute.spec.json create mode 100644 shared/contracts-raw/escrow.spec.json diff --git a/scripts/generate-contract-bindings.js b/scripts/generate-contract-bindings.js new file mode 100644 index 0000000..db99e69 --- /dev/null +++ b/scripts/generate-contract-bindings.js @@ -0,0 +1,245 @@ +#!/usr/bin/env node +/** + * Soroban Contract Bindings Codegen + * + * Reads Soroban contract spec JSON files and generates fully typed + * TypeScript client bindings for compile-time checked contract calls. + * + * Usage: node scripts/generate-contract-bindings.js + * npm run codegen + */ +const fs = require('fs') +const path = require('path') + +const SPECS_DIR = path.resolve(__dirname, '../shared/contracts-raw') +const OUTPUT_DIR = path.resolve(__dirname, '../shared/contracts-gen') + +// Soroban native types to TypeScript mappings +const TYPE_MAP = { + Void: 'void', + Bool: 'boolean', + U32: 'number', + U64: 'bigint', + I32: 'number', + I128: 'bigint', + String: 'string', + Bytes: 'Uint8Array', + Address: 'string', +} + +function mapType(typeDef, localTypes) { + if (!typeDef || !typeDef.type) return 'unknown' + + const raw = typeDef.type + + // Check if it's a native Soroban type + if (TYPE_MAP[raw]) return TYPE_MAP[raw] + + // Check for generic types: Vec, Option, Map + const vecMatch = raw.match(/^Vec<(.+)>$/) + if (vecMatch) { + return `${mapType({ type: vecMatch[1] }, localTypes)}[]` + } + + const optionMatch = raw.match(/^Option<(.+)>$/) + if (optionMatch) { + return `${mapType({ type: optionMatch[1] }, localTypes)} | null` + } + + const bytesNMatch = raw.match(/^BytesN<(\d+)>$/) + if (bytesNMatch) { + return 'Uint8Array' + } + + // Check if it's a local custom type + if (localTypes && localTypes[raw]) { + return raw + } + + return 'unknown' +} + +function generateTypeInterface(name, typeDef, localTypes) { + if (typeDef.variants) { + // Generate an enum/union type + const variants = typeDef.variants.map(v => ` ${v}`).join(' |\n') + return `export type ${name} =\n${variants}\n` + } + + if (typeDef.fields) { + const fields = Object.entries(typeDef.fields) + .map(([field, def]) => ` ${field}: ${mapType(def, localTypes)}`) + .join('\n') + return `export interface ${name} {\n${fields}\n}\n` + } + + return '' +} + +function generateMethodArgs(args, localTypes) { + if (!args || Object.keys(args).length === 0) return '' + return Object.entries(args) + .map(([name, def]) => `${name}: ${mapType(def, localTypes)}`) + .join(', ') +} + +function generateMethod(name, methodDef, localTypes) { + const args = generateMethodArgs(methodDef.args, localTypes) + const resultType = methodDef.result ? mapType(methodDef.result, localTypes) : 'void' + const jsDoc = methodDef.description ? ` /** ${methodDef.description} */\n` : '' + + return `${jsDoc} ${name}(${args}): Promise<${resultType}>` +} + +function generateEventDataType(name, dataDef, localTypes) { + if (!dataDef || Object.keys(dataDef).length === 0) return 'void' + const fields = Object.entries(dataDef) + .map(([field, def]) => ` ${field}: ${mapType(def, localTypes)}`) + .join('\n') + return `{\n${fields}\n}` +} + +function generateBindings(spec) { + const lines = [] + const contractName = spec.contract.charAt(0).toUpperCase() + spec.contract.slice(1) + + lines.push(`/**`) + lines.push(` * Auto-generated ${contractName} contract bindings`) + lines.push(` * Generated from Soroban contract spec — do not edit manually`) + lines.push(` */`) + lines.push(`import { Contract, rpc, xdr, Address } from '@stellar/stellar-sdk'`) + lines.push(`import { getSorobanServer } from '../soroban-rpc'`) + lines.push(`import { ${spec.contract.toUpperCase()}_CONTRACT_ID } from '../contracts'`) + lines.push(``) + + // Generate type definitions + lines.push(`// ── Types ──────────────────────────────────────────────────────`) + lines.push(``) + for (const [typeName, typeDef] of Object.entries(spec.types || {})) { + lines.push(generateTypeInterface(typeName, typeDef, spec.types)) + } + + // Generate event data types + if (spec.events) { + lines.push(`// ── Event Data Types ───────────────────────────────────────────`) + lines.push(``) + for (const [eventName, eventDef] of Object.entries(spec.events)) { + const dataType = generateEventDataType(eventName, eventDef.data, spec.types) + lines.push(`export interface ${contractName}${eventName.charAt(0).toUpperCase() + eventName.slice(1)}EventData ${dataType}`) + lines.push(``) + } + } + + // Generate contract client interface + lines.push(`// ── Contract Interface ──────────────────────────────────────────`) + lines.push(``) + lines.push(`export interface I${contractName}Contract {`) + for (const [methodName, methodDef] of Object.entries(spec.methods)) { + lines.push(generateMethod(methodName, methodDef, spec.types)) + } + lines.push(`}`) + + // Generate contract client implementation + lines.push(``) + lines.push(`// ── Contract Client ─────────────────────────────────────────────`) + lines.push(``) + lines.push(`export function create${contractName}Contract(): I${contractName}Contract {`) + lines.push(` const server = getSorobanServer()`) + lines.push(` const contractId = ${spec.contract.toUpperCase()}_CONTRACT_ID`) + lines.push(` const contract = new Contract(contractId)`) + lines.push(``) + lines.push(` async function invoke(method: string, ...args: xdr.ScVal[]): Promise {`) + lines.push(` const tx = await server.simulateTransaction(contract.call(method, ...args))`) + lines.push(` const result = rpc.assembleTransaction(contract.call(method, ...args), tx).toXDR()`) + lines.push(` const sendResponse = await server.sendTransaction(result)`) + lines.push(` const resultResponse = await server.getTransaction(sendResponse.hash)`) + lines.push(` if (resultResponse.status !== 'SUCCESS') {`) + lines.push(` throw new Error(\`Transaction failed: \${resultResponse.status}\`)`) + lines.push(` }`) + lines.push(` const returnValue = contract.call(method, ...args).result?.val`) + lines.push(` return rpc.scValToNative(returnValue as xdr.ScVal) as T`) + lines.push(` }`) + lines.push(``) + lines.push(` return {`) + + // Generate method implementations + for (const [methodName, methodDef] of Object.entries(spec.methods)) { + const args = Object.entries(methodDef.args || {}) + .map(([name, def]) => { + const tsType = mapType(def, spec.types) + return `${name}: ${tsType}` + }) + .join(', ') + + const scVals = Object.entries(methodDef.args || {}) + .map(([name, def]) => { + if (def.type === 'Address') return `new Address(${name}).toScVal()` + if (def.type === 'I128') return `xdr.ScVal.scvI128(new xdr.Int128Parts({ hi: xdr.Int64.fromString(String(BigInt(${name}) >> 64n)), lo: xdr.Int64.fromString(String(BigInt(${name}) & 0xFFFFFFFFFFFFFFFFn)) }))` + if (def.type === 'U32') return `xdr.ScVal.scvU32(${name})` + if (def.type === 'U64') return `xdr.ScVal.scvU64(xdr.Int64.fromString(String(${name})))` + if (def.type === 'Bool') return `xdr.ScVal.scvBool(${name})` + if (def.type === 'String') return `xdr.ScVal.scvString(${name})` + if (def.type === 'BytesN<32>') return `xdr.ScVal.scvBytes(Uint8Array.from(${name}))` + return `xdr.ScVal.scvBytes(${name})` + }) + .join(', ') + + const resultType = methodDef.result ? mapType(methodDef.result, spec.types) : 'void' + + lines.push(` ${methodName}: (${args}): Promise<${resultType}> => {`) + lines.push(` return invoke<${resultType}>(`) + lines.push(` '${methodName}',`) + lines.push(` ${scVals || '// no args'}`) + lines.push(` )`) + lines.push(` },`) + lines.push(``) + } + + lines.push(` }`) + lines.push(`}`) + lines.push(``) + + return lines.join('\n') +} + +function main() { + const specFiles = fs.readdirSync(SPECS_DIR).filter(f => f.endsWith('.spec.json')) + + if (specFiles.length === 0) { + console.log('No spec files found in', SPECS_DIR) + process.exit(1) + } + + // Ensure output directory exists + if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }) + } + + const indexLines = [] + indexLines.push(`/**`) + indexLines.push(` * Auto-generated contract bindings barrel export`) + indexLines.push(` * Generated from Soroban contract specs — do not edit manually`) + indexLines.push(` */`) + indexLines.push(``) + + for (const specFile of specFiles) { + const specPath = path.join(SPECS_DIR, specFile) + const spec = JSON.parse(fs.readFileSync(specPath, 'utf-8')) + const contractName = spec.contract.charAt(0).toUpperCase() + spec.contract.slice(1) + + const output = generateBindings(spec) + const outputPath = path.join(OUTPUT_DIR, `${spec.contract}.ts`) + fs.writeFileSync(outputPath, output, 'utf-8') + console.log(`Generated: ${outputPath}`) + + indexLines.push(`export * from './${spec.contract}'`) + } + + // Generate barrel export + const indexPath = path.join(OUTPUT_DIR, 'index.ts') + fs.writeFileSync(indexPath, indexLines.join('\n'), 'utf-8') + console.log(`Generated: ${indexPath}`) + console.log(`\nSuccessfully generated bindings for ${specFiles.length} contract(s)`) +} + +main() diff --git a/shared/contracts-raw/dispute.spec.json b/shared/contracts-raw/dispute.spec.json new file mode 100644 index 0000000..1ada941 --- /dev/null +++ b/shared/contracts-raw/dispute.spec.json @@ -0,0 +1,103 @@ +{ + "contract": "dispute", + "address_env": "NEXT_PUBLIC_DISPUTE_CONTRACT_ID", + "methods": { + "open_dispute": { + "description": "Open a dispute for a gig milestone", + "args": { + "gig_id": { "type": "BytesN<32>" }, + "milestone_index": { "type": "U32" }, + "reason": { "type": "String" } + }, + "result": { "type": "Void" }, + "event": "dispute_opened" + }, + "submit_evidence": { + "description": "Submit evidence for a dispute", + "args": { + "dispute_id": { "type": "BytesN<32>" }, + "evidence_uri": { "type": "String" } + }, + "result": { "type": "Void" }, + "event": "evidence_submitted" + }, + "vote": { + "description": "Cast a juror vote on a dispute", + "args": { + "dispute_id": { "type": "BytesN<32>" }, + "in_favor": { "type": "Bool" } + }, + "result": { "type": "Void" }, + "event": "vote_cast" + }, + "resolve": { + "description": "Resolve a dispute and execute the outcome", + "args": { + "dispute_id": { "type": "BytesN<32>" } + }, + "result": { "type": "DisputeOutcome" }, + "event": "dispute_resolved" + }, + "get_dispute": { + "description": "Get dispute details", + "args": { + "dispute_id": { "type": "BytesN<32>" } + }, + "result": { "type": "Dispute" } + } + }, + "types": { + "Dispute": { + "fields": { + "id": { "type": "BytesN<32>" }, + "gig_id": { "type": "BytesN<32>" }, + "milestone_index": { "type": "U32" }, + "opener": { "type": "Address" }, + "reason": { "type": "String" }, + "status": { "type": "DisputeStatus" }, + "votes_for": { "type": "U32" }, + "votes_against": { "type": "U32" }, + "created_at": { "type": "U64" }, + "resolved_at": { "type": "Option" } + } + }, + "DisputeStatus": { + "variants": ["Open", "Voting", "Resolved", "Executed"] + }, + "DisputeOutcome": { + "variants": ["FreelancerWins", "ClientWins", "Split"] + } + }, + "events": { + "dispute_opened": { + "topics": ["dispute_opened", "gig_id"], + "data": { + "dispute_id": { "type": "BytesN<32>" }, + "opener": { "type": "Address" }, + "reason": { "type": "String" } + } + }, + "evidence_submitted": { + "topics": ["evidence_submitted", "dispute_id"], + "data": { + "submitter": { "type": "Address" }, + "evidence_uri": { "type": "String" } + } + }, + "vote_cast": { + "topics": ["vote_cast", "dispute_id"], + "data": { + "voter": { "type": "Address" }, + "in_favor": { "type": "Bool" } + } + }, + "dispute_resolved": { + "topics": ["dispute_resolved", "dispute_id"], + "data": { + "outcome": { "type": "DisputeOutcome" }, + "votes_for": { "type": "U32" }, + "votes_against": { "type": "U32" } + } + } + } +} diff --git a/shared/contracts-raw/escrow.spec.json b/shared/contracts-raw/escrow.spec.json new file mode 100644 index 0000000..ac05739 --- /dev/null +++ b/shared/contracts-raw/escrow.spec.json @@ -0,0 +1,93 @@ +{ + "contract": "escrow", + "address_env": "NEXT_PUBLIC_ESCROW_CONTRACT_ID", + "methods": { + "deposit": { + "description": "Deposit funds into escrow for a gig milestone", + "args": { + "gig_id": { "type": "BytesN<32>" }, + "milestone_index": { "type": "U32" }, + "token": { "type": "Address" }, + "amount": { "type": "I128" } + }, + "result": { "type": "Void" }, + "event": "deposited" + }, + "release": { + "description": "Release escrowed funds to the freelancer", + "args": { + "gig_id": { "type": "BytesN<32>" }, + "milestone_index": { "type": "U32" } + }, + "result": { "type": "Void" }, + "event": "released" + }, + "refund": { + "description": "Refund escrowed funds to the client", + "args": { + "gig_id": { "type": "BytesN<32>" }, + "milestone_index": { "type": "U32" } + }, + "result": { "type": "Void" }, + "event": "refunded" + }, + "get_balance": { + "description": "Get the escrowed balance for a milestone", + "args": { + "gig_id": { "type": "BytesN<32>" }, + "milestone_index": { "type": "U32" } + }, + "result": { "type": "I128" } + }, + "get_gig_milestones": { + "description": "Get all milestones for a gig", + "args": { + "gig_id": { "type": "BytesN<32>" } + }, + "result": { + "type": "Vec" + } + } + }, + "types": { + "Milestone": { + "fields": { + "index": { "type": "U32" }, + "amount": { "type": "I128" }, + "token": { "type": "Address" }, + "status": { "type": "MilestoneStatus" }, + "freelancer": { "type": "Address" } + } + }, + "MilestoneStatus": { + "variants": ["Pending", "Funded", "Completed", "Released", "Disputed", "Refunded"] + } + }, + "events": { + "deposited": { + "topics": ["deposited", "gig_id"], + "data": { + "milestone_index": { "type": "U32" }, + "token": { "type": "Address" }, + "amount": { "type": "I128" }, + "depositor": { "type": "Address" } + } + }, + "released": { + "topics": ["released", "gig_id"], + "data": { + "milestone_index": { "type": "U32" }, + "freelancer": { "type": "Address" }, + "amount": { "type": "I128" } + } + }, + "refunded": { + "topics": ["refunded", "gig_id"], + "data": { + "milestone_index": { "type": "U32" }, + "client": { "type": "Address" }, + "amount": { "type": "I128" } + } + } + } +} From ab69678997a7720f5b7352b91314af856914c802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Crhemy-arc=E2=80=9D?= <“rhemaadzer@gmail.com”> Date: Tue, 21 Jul 2026 19:56:41 +0100 Subject: [PATCH 02/11] feat(codegen): generate type-safe contract bindings from specs Generate TypeScript client bindings for escrow and dispute contracts. Each contract gets a typed interface, implementation, and event data types. Bindings are auto-generated from spec files and should not be edited manually. --- shared/contracts-gen/dispute.ts | 130 ++++++++++++++++++++++++++++++++ shared/contracts-gen/escrow.ts | 119 +++++++++++++++++++++++++++++ shared/contracts-gen/index.ts | 7 ++ 3 files changed, 256 insertions(+) create mode 100644 shared/contracts-gen/dispute.ts create mode 100644 shared/contracts-gen/escrow.ts create mode 100644 shared/contracts-gen/index.ts diff --git a/shared/contracts-gen/dispute.ts b/shared/contracts-gen/dispute.ts new file mode 100644 index 0000000..af40f4d --- /dev/null +++ b/shared/contracts-gen/dispute.ts @@ -0,0 +1,130 @@ +/** + * Auto-generated Dispute contract bindings + * Generated from Soroban contract spec — do not edit manually + */ +import { Contract, rpc, xdr, Address } from '@stellar/stellar-sdk' +import { getSorobanServer } from '../soroban-rpc' +import { DISPUTE_CONTRACT_ID } from '../contracts' + +// ── Types ────────────────────────────────────────────────────── + +export interface Dispute { + id: Uint8Array + gig_id: Uint8Array + milestone_index: number + opener: string + reason: string + status: DisputeStatus + votes_for: number + votes_against: number + created_at: bigint + resolved_at: bigint | null +} + +export type DisputeStatus = + Open | + Voting | + Resolved | + Executed + +export type DisputeOutcome = + FreelancerWins | + ClientWins | + Split + +// ── Event Data Types ─────────────────────────────────────────── + +export interface DisputeDispute_openedEventData { + dispute_id: Uint8Array + opener: string + reason: string +} + +export interface DisputeEvidence_submittedEventData { + submitter: string + evidence_uri: string +} + +export interface DisputeVote_castEventData { + voter: string + in_favor: boolean +} + +export interface DisputeDispute_resolvedEventData { + outcome: DisputeOutcome + votes_for: number + votes_against: number +} + +// ── Contract Interface ────────────────────────────────────────── + +export interface IDisputeContract { + /** Open a dispute for a gig milestone */ + open_dispute(gig_id: Uint8Array, milestone_index: number, reason: string): Promise + /** Submit evidence for a dispute */ + submit_evidence(dispute_id: Uint8Array, evidence_uri: string): Promise + /** Cast a juror vote on a dispute */ + vote(dispute_id: Uint8Array, in_favor: boolean): Promise + /** Resolve a dispute and execute the outcome */ + resolve(dispute_id: Uint8Array): Promise + /** Get dispute details */ + get_dispute(dispute_id: Uint8Array): Promise +} + +// ── Contract Client ───────────────────────────────────────────── + +export function createDisputeContract(): IDisputeContract { + const server = getSorobanServer() + const contractId = DISPUTE_CONTRACT_ID + const contract = new Contract(contractId) + + async function invoke(method: string, ...args: xdr.ScVal[]): Promise { + const tx = await server.simulateTransaction(contract.call(method, ...args)) + const result = rpc.assembleTransaction(contract.call(method, ...args), tx).toXDR() + const sendResponse = await server.sendTransaction(result) + const resultResponse = await server.getTransaction(sendResponse.hash) + if (resultResponse.status !== 'SUCCESS') { + throw new Error(`Transaction failed: ${resultResponse.status}`) + } + const returnValue = contract.call(method, ...args).result?.val + return rpc.scValToNative(returnValue as xdr.ScVal) as T + } + + return { + open_dispute: (gig_id: Uint8Array, milestone_index: number, reason: string): Promise => { + return invoke( + 'open_dispute', + xdr.ScVal.scvBytes(Uint8Array.from(gig_id)), xdr.ScVal.scvU32(milestone_index), xdr.ScVal.scvString(reason) + ) + }, + + submit_evidence: (dispute_id: Uint8Array, evidence_uri: string): Promise => { + return invoke( + 'submit_evidence', + xdr.ScVal.scvBytes(Uint8Array.from(dispute_id)), xdr.ScVal.scvString(evidence_uri) + ) + }, + + vote: (dispute_id: Uint8Array, in_favor: boolean): Promise => { + return invoke( + 'vote', + xdr.ScVal.scvBytes(Uint8Array.from(dispute_id)), xdr.ScVal.scvBool(in_favor) + ) + }, + + resolve: (dispute_id: Uint8Array): Promise => { + return invoke( + 'resolve', + xdr.ScVal.scvBytes(Uint8Array.from(dispute_id)) + ) + }, + + get_dispute: (dispute_id: Uint8Array): Promise => { + return invoke( + 'get_dispute', + xdr.ScVal.scvBytes(Uint8Array.from(dispute_id)) + ) + }, + + } +} diff --git a/shared/contracts-gen/escrow.ts b/shared/contracts-gen/escrow.ts new file mode 100644 index 0000000..fb8ee7b --- /dev/null +++ b/shared/contracts-gen/escrow.ts @@ -0,0 +1,119 @@ +/** + * Auto-generated Escrow contract bindings + * Generated from Soroban contract spec — do not edit manually + */ +import { Contract, rpc, xdr, Address } from '@stellar/stellar-sdk' +import { getSorobanServer } from '../soroban-rpc' +import { ESCROW_CONTRACT_ID } from '../contracts' + +// ── Types ────────────────────────────────────────────────────── + +export interface Milestone { + index: number + amount: bigint + token: string + status: MilestoneStatus + freelancer: string +} + +export type MilestoneStatus = + Pending | + Funded | + Completed | + Released | + Disputed | + Refunded + +// ── Event Data Types ─────────────────────────────────────────── + +export interface EscrowDepositedEventData { + milestone_index: number + token: string + amount: bigint + depositor: string +} + +export interface EscrowReleasedEventData { + milestone_index: number + freelancer: string + amount: bigint +} + +export interface EscrowRefundedEventData { + milestone_index: number + client: string + amount: bigint +} + +// ── Contract Interface ────────────────────────────────────────── + +export interface IEscrowContract { + /** Deposit funds into escrow for a gig milestone */ + deposit(gig_id: Uint8Array, milestone_index: number, token: string, amount: bigint): Promise + /** Release escrowed funds to the freelancer */ + release(gig_id: Uint8Array, milestone_index: number): Promise + /** Refund escrowed funds to the client */ + refund(gig_id: Uint8Array, milestone_index: number): Promise + /** Get the escrowed balance for a milestone */ + get_balance(gig_id: Uint8Array, milestone_index: number): Promise + /** Get all milestones for a gig */ + get_gig_milestones(gig_id: Uint8Array): Promise +} + +// ── Contract Client ───────────────────────────────────────────── + +export function createEscrowContract(): IEscrowContract { + const server = getSorobanServer() + const contractId = ESCROW_CONTRACT_ID + const contract = new Contract(contractId) + + async function invoke(method: string, ...args: xdr.ScVal[]): Promise { + const tx = await server.simulateTransaction(contract.call(method, ...args)) + const result = rpc.assembleTransaction(contract.call(method, ...args), tx).toXDR() + const sendResponse = await server.sendTransaction(result) + const resultResponse = await server.getTransaction(sendResponse.hash) + if (resultResponse.status !== 'SUCCESS') { + throw new Error(`Transaction failed: ${resultResponse.status}`) + } + const returnValue = contract.call(method, ...args).result?.val + return rpc.scValToNative(returnValue as xdr.ScVal) as T + } + + return { + deposit: (gig_id: Uint8Array, milestone_index: number, token: string, amount: bigint): Promise => { + return invoke( + 'deposit', + xdr.ScVal.scvBytes(Uint8Array.from(gig_id)), xdr.ScVal.scvU32(milestone_index), new Address(token).toScVal(), xdr.ScVal.scvI128(new xdr.Int128Parts({ hi: xdr.Int64.fromString(String(BigInt(amount) >> 64n)), lo: xdr.Int64.fromString(String(BigInt(amount) & 0xFFFFFFFFFFFFFFFFn)) })) + ) + }, + + release: (gig_id: Uint8Array, milestone_index: number): Promise => { + return invoke( + 'release', + xdr.ScVal.scvBytes(Uint8Array.from(gig_id)), xdr.ScVal.scvU32(milestone_index) + ) + }, + + refund: (gig_id: Uint8Array, milestone_index: number): Promise => { + return invoke( + 'refund', + xdr.ScVal.scvBytes(Uint8Array.from(gig_id)), xdr.ScVal.scvU32(milestone_index) + ) + }, + + get_balance: (gig_id: Uint8Array, milestone_index: number): Promise => { + return invoke( + 'get_balance', + xdr.ScVal.scvBytes(Uint8Array.from(gig_id)), xdr.ScVal.scvU32(milestone_index) + ) + }, + + get_gig_milestones: (gig_id: Uint8Array): Promise => { + return invoke( + 'get_gig_milestones', + xdr.ScVal.scvBytes(Uint8Array.from(gig_id)) + ) + }, + + } +} diff --git a/shared/contracts-gen/index.ts b/shared/contracts-gen/index.ts new file mode 100644 index 0000000..10ade70 --- /dev/null +++ b/shared/contracts-gen/index.ts @@ -0,0 +1,7 @@ +/** + * Auto-generated contract bindings barrel export + * Generated from Soroban contract specs — do not edit manually + */ + +export * from './dispute' +export * from './escrow' \ No newline at end of file From ec462a81cf843c5e6671671a57fac0478810c65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Crhemy-arc=E2=80=9D?= <“rhemaadzer@gmail.com”> Date: Tue, 21 Jul 2026 19:57:16 +0100 Subject: [PATCH 03/11] feat(codegen): integrate codegen into build and dev scripts Add codegen script to package.json and wire it into build pipeline. Running npm run build or npm run dev now automatically regenerates contract bindings from specs before starting. --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index bff0ca6..bb6021d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build", + "predev": "npm run codegen", + "build": "npm run codegen && next build", "start": "next start", "lint": "next lint", "typecheck": "tsc --noEmit", @@ -15,6 +16,7 @@ "bindings:crowdfund": "./target/bin/soroban contract bindings typescript --wasm ./target/wasm32-unknown-unknown/release/soroban_crowdfund_contract.wasm --id $(cat ./.soroban-example-dapp/crowdfund_id) --output-dir ./.soroban-example-dapp/crowdfund-contract --network $(cat ./.soroban-example-dapp/network) --overwrite", "bindings:abundance": "./target/bin/soroban contract bindings typescript --wasm ./target/wasm32-unknown-unknown/release/abundance_token.wasm --id $(cat ./.soroban-example-dapp/abundance_token_id) --output-dir ./.soroban-example-dapp/abundance-token --network $(cat ./.soroban-example-dapp/network) --overwrite", "bindings": "npm run bindings:crowdfund && npm run bindings:abundance", + "codegen": "node scripts/generate-contract-bindings.js", "convert:assets": "node scripts/convert-to-webp.js" }, "dependencies": { From 4aa65f255bb374988b6ae038d7f22a42547f282e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Crhemy-arc=E2=80=9D?= <“rhemaadzer@gmail.com”> Date: Tue, 21 Jul 2026 19:57:34 +0100 Subject: [PATCH 04/11] chore: exclude auto-generated contract bindings from version control Add shared/contracts-gen to .gitignore since it is regenerated from specs via npm run codegen. Keeps the repo clean and avoids merge conflicts on generated files. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 39532d3..fcdd599 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ yarn-error.log* *.sublime-workspace # Local scripts output + +# Auto-generated contract bindings (regenerated via npm run codegen) +/shared/contracts-gen From 7558434ee3d4e1603b5aa0eea73e73f0daab8935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Crhemy-arc=E2=80=9D?= <“rhemaadzer@gmail.com”> Date: Tue, 21 Jul 2026 19:58:19 +0100 Subject: [PATCH 05/11] feat(hooks): add type-safe contract interaction hooks Add useEscrowContract and useDisputeContract hooks that wrap the auto-generated contract bindings. These provide React-friendly methods with error handling and ready state for compile-time checked contract calls. --- hooks/index.ts | 2 + hooks/useDisputeContract.ts | 100 ++++++++++++++++++++++++++++++++++++ hooks/useEscrowContract.ts | 99 +++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 hooks/useDisputeContract.ts create mode 100644 hooks/useEscrowContract.ts diff --git a/hooks/index.ts b/hooks/index.ts index cca8547..e447294 100644 --- a/hooks/index.ts +++ b/hooks/index.ts @@ -1,5 +1,7 @@ export * from "./useAccount"; export * from "./useDebouncedValue"; +export * from "./useEscrowContract"; +export * from "./useDisputeContract"; export * from "./useGigsExplorer"; export * from "./useWallet"; export * from "./useIsMounted"; diff --git a/hooks/useDisputeContract.ts b/hooks/useDisputeContract.ts new file mode 100644 index 0000000..a4cc01d --- /dev/null +++ b/hooks/useDisputeContract.ts @@ -0,0 +1,100 @@ +import { useCallback, useState } from 'react' +import { + createDisputeContract, + type Dispute, + type DisputeOutcome, + type DisputeStatus, +} from '../shared/contracts-gen/dispute' +import { DISPUTE_CONTRACT_ID } from '../shared/contracts' + +export type { Dispute, DisputeOutcome, DisputeStatus } + +interface UseDisputeContractResult { + openDispute: (gigId: Uint8Array, milestoneIndex: number, reason: string) => Promise + submitEvidence: (disputeId: Uint8Array, evidenceUri: string) => Promise + vote: (disputeId: Uint8Array, inFavor: boolean) => Promise + resolve: (disputeId: Uint8Array) => Promise + getDispute: (disputeId: Uint8Array) => Promise + isReady: boolean + error: string | null + clearError: () => void +} + +/** + * React hook for interacting with the dispute contract. + * + * Provides type-safe methods for opening disputes, submitting evidence, + * voting, and resolving disputes. All methods return typed results that + * are compile-time checked against the contract spec. + */ +export function useDisputeContract(): UseDisputeContractResult { + const [error, setError] = useState(null) + const isReady = !!DISPUTE_CONTRACT_ID + + const contract = isReady ? createDisputeContract() : null + + const withErrorHandling = useCallback( + (fn: () => Promise): Promise => { + setError(null) + return fn().catch((err) => { + const message = err instanceof Error ? err.message : 'Contract call failed' + setError(message) + throw err + }) + }, + [] + ) + + const openDispute = useCallback( + (gigId: Uint8Array, milestoneIndex: number, reason: string) => { + if (!contract) throw new Error('Dispute contract not configured') + return withErrorHandling(() => contract.open_dispute(gigId, milestoneIndex, reason)) + }, + [contract, withErrorHandling] + ) + + const submitEvidence = useCallback( + (disputeId: Uint8Array, evidenceUri: string) => { + if (!contract) throw new Error('Dispute contract not configured') + return withErrorHandling(() => contract.submit_evidence(disputeId, evidenceUri)) + }, + [contract, withErrorHandling] + ) + + const vote = useCallback( + (disputeId: Uint8Array, inFavor: boolean) => { + if (!contract) throw new Error('Dispute contract not configured') + return withErrorHandling(() => contract.vote(disputeId, inFavor)) + }, + [contract, withErrorHandling] + ) + + const resolve = useCallback( + (disputeId: Uint8Array) => { + if (!contract) throw new Error('Dispute contract not configured') + return withErrorHandling(() => contract.resolve(disputeId)) + }, + [contract, withErrorHandling] + ) + + const getDispute = useCallback( + (disputeId: Uint8Array) => { + if (!contract) throw new Error('Dispute contract not configured') + return withErrorHandling(() => contract.get_dispute(disputeId)) + }, + [contract, withErrorHandling] + ) + + const clearError = useCallback(() => setError(null), []) + + return { + openDispute, + submitEvidence, + vote, + resolve, + getDispute, + isReady, + error, + clearError, + } +} diff --git a/hooks/useEscrowContract.ts b/hooks/useEscrowContract.ts new file mode 100644 index 0000000..2db7215 --- /dev/null +++ b/hooks/useEscrowContract.ts @@ -0,0 +1,99 @@ +import { useCallback, useState } from 'react' +import { + createEscrowContract, + type Milestone, + type MilestoneStatus, +} from '../shared/contracts-gen/escrow' +import { ESCROW_CONTRACT_ID } from '../shared/contracts' + +export type { Milestone, MilestoneStatus } + +interface UseEscrowContractResult { + deposit: (gigId: Uint8Array, milestoneIndex: number, token: string, amount: bigint) => Promise + release: (gigId: Uint8Array, milestoneIndex: number) => Promise + refund: (gigId: Uint8Array, milestoneIndex: number) => Promise + getBalance: (gigId: Uint8Array, milestoneIndex: number) => Promise + getMilestones: (gigId: Uint8Array) => Promise + isReady: boolean + error: string | null + clearError: () => void +} + +/** + * React hook for interacting with the escrow contract. + * + * Provides type-safe methods for depositing, releasing, and refunding + * escrowed funds. All methods return typed results that are compile-time + * checked against the contract spec. + */ +export function useEscrowContract(): UseEscrowContractResult { + const [error, setError] = useState(null) + const isReady = !!ESCROW_CONTRACT_ID + + const contract = isReady ? createEscrowContract() : null + + const withErrorHandling = useCallback( + (fn: () => Promise): Promise => { + setError(null) + return fn().catch((err) => { + const message = err instanceof Error ? err.message : 'Contract call failed' + setError(message) + throw err + }) + }, + [] + ) + + const deposit = useCallback( + (gigId: Uint8Array, milestoneIndex: number, token: string, amount: bigint) => { + if (!contract) throw new Error('Escrow contract not configured') + return withErrorHandling(() => contract.deposit(gigId, milestoneIndex, token, amount)) + }, + [contract, withErrorHandling] + ) + + const release = useCallback( + (gigId: Uint8Array, milestoneIndex: number) => { + if (!contract) throw new Error('Escrow contract not configured') + return withErrorHandling(() => contract.release(gigId, milestoneIndex)) + }, + [contract, withErrorHandling] + ) + + const refund = useCallback( + (gigId: Uint8Array, milestoneIndex: number) => { + if (!contract) throw new Error('Escrow contract not configured') + return withErrorHandling(() => contract.refund(gigId, milestoneIndex)) + }, + [contract, withErrorHandling] + ) + + const getBalance = useCallback( + (gigId: Uint8Array, milestoneIndex: number) => { + if (!contract) throw new Error('Escrow contract not configured') + return withErrorHandling(() => contract.get_balance(gigId, milestoneIndex)) + }, + [contract, withErrorHandling] + ) + + const getMilestones = useCallback( + (gigId: Uint8Array) => { + if (!contract) throw new Error('Escrow contract not configured') + return withErrorHandling(() => contract.get_gig_milestones(gigId)) + }, + [contract, withErrorHandling] + ) + + const clearError = useCallback(() => setError(null), []) + + return { + deposit, + release, + refund, + getBalance, + getMilestones, + isReady, + error, + clearError, + } +} From b2effd7aec0d5bd5f60147aacbebfbf11088c183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Crhemy-arc=E2=80=9D?= <“rhemaadzer@gmail.com”> Date: Tue, 21 Jul 2026 19:58:53 +0100 Subject: [PATCH 06/11] feat(form-pledge): integrate typed escrow contract for deposits Update FormPledge to use useEscrowContract hook for type-safe deposit calls. Falls back to simulated delay when contract is not configured. Adds gigId and milestoneIndex props for contract interaction. --- components/molecules/form-pledge/index.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/components/molecules/form-pledge/index.tsx b/components/molecules/form-pledge/index.tsx index 2cd0b5b..af7d1f6 100644 --- a/components/molecules/form-pledge/index.tsx +++ b/components/molecules/form-pledge/index.tsx @@ -2,6 +2,7 @@ import React, { FunctionComponent, useState } from 'react' import { AmountInput, Button, Checkbox } from '../../atoms' import { TransactionModal } from '../../molecules/transaction-modal' import { Utils } from '../../../shared/utils' +import { useEscrowContract } from '../../../hooks' import styles from './style.module.css' import { Spacer } from '../../atoms/spacer' @@ -9,6 +10,8 @@ export interface IFormPledgeProps { account: string decimals: number symbol?: string + gigId?: Uint8Array + milestoneIndex?: number onPledge: () => void updatedAt: number } @@ -23,8 +26,9 @@ export interface IResultSubmit { /** * FormPledge — escrow deposit form. * - * TODO: replace the simulated submit with a real TrustFlow contract call - * once RPC integration is wired up in shared/contracts.ts. + * Uses the auto-generated escrow contract bindings for type-safe + * deposit calls. Falls back to simulated delay if contract is not + * configured. */ const FormPledge: FunctionComponent = props => { const [balance] = React.useState(BigInt(0)) @@ -36,6 +40,8 @@ const FormPledge: FunctionComponent = props => { const [input, setInput] = useState('') const [isSubmitting, setSubmitting] = useState(false) + const escrow = useEscrowContract() + const clearInput = (): void => { setInput('') } @@ -44,8 +50,12 @@ const FormPledge: FunctionComponent = props => { if (!amount) return setSubmitting(true) try { - // TODO: call TrustFlow contract deposit - await new Promise(r => setTimeout(r, 800)) // simulated delay + if (escrow.isReady && props.gigId && props.milestoneIndex !== undefined) { + const amountBigInt = BigInt(Math.floor(amount * 10 ** decimals)) + await escrow.deposit(props.gigId, props.milestoneIndex, props.symbol ?? 'XLM', amountBigInt) + } else { + await new Promise(r => setTimeout(r, 800)) + } setResultSubmit({ status: 'success', value: String(amount), symbol }) props.onPledge() setInput('') From 3f55480bdd2d5172bc6473b6d11840803acd6eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Crhemy-arc=E2=80=9D?= <“rhemaadzer@gmail.com”> Date: Tue, 21 Jul 2026 19:59:27 +0100 Subject: [PATCH 07/11] feat(deposits): integrate typed escrow contract for balance fetching Update Deposits component to use useEscrowContract hook for fetching real escrowed balance. Adds gigId and milestoneIndex props. Falls back to zero when contract is not configured. --- components/molecules/deposits/index.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/components/molecules/deposits/index.tsx b/components/molecules/deposits/index.tsx index 3fa58f2..8cfcce8 100644 --- a/components/molecules/deposits/index.tsx +++ b/components/molecules/deposits/index.tsx @@ -1,10 +1,13 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' import { Spacer } from '../../atoms/spacer' import { Utils } from '../../../shared/utils' +import { useEscrowContract } from '../../../hooks' export interface IDepositsProps { address: string decimals: number + gigId?: Uint8Array + milestoneIndex?: number name?: string symbol?: string } @@ -12,12 +15,18 @@ export interface IDepositsProps { /** * Deposits — shows the user's escrowed balance. * - * TODO: fetch real balance from the TrustFlow contract once RPC integration - * is wired up in shared/contracts.ts. + * Fetches real balance from the escrow contract when configured. + * Falls back to zero if contract is not available. */ export function Deposits(props: IDepositsProps) { - // Placeholder — will be replaced with contract call - const balance = BigInt(0) + const [balance, setBalance] = useState(BigInt(0)) + const escrow = useEscrowContract() + + useEffect(() => { + if (!escrow.isReady || !props.gigId || props.milestoneIndex === undefined) return + + escrow.getBalance(props.gigId, props.milestoneIndex).then(setBalance).catch(() => {}) + }, [escrow, props.gigId, props.milestoneIndex]) if (Number(balance) <= 0) { return From 6672fec75337d46ae2eec652c8204d7594892bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Crhemy-arc=E2=80=9D?= <“rhemaadzer@gmail.com”> Date: Tue, 21 Jul 2026 19:59:49 +0100 Subject: [PATCH 08/11] feat(types): add typed event data interfaces for contract events Add EscrowEvent and DisputeEvent interfaces that define the shape of event data for each contract. This enables compile-time checking of event handlers when consuming contract events. --- shared/contract-events/types.ts | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/shared/contract-events/types.ts b/shared/contract-events/types.ts index 11b9c92..71972a1 100644 --- a/shared/contract-events/types.ts +++ b/shared/contract-events/types.ts @@ -6,6 +6,58 @@ */ export type ContractEventSource = 'escrow' | 'dispute' +/** + * Typed event data for escrow contract events. + * Import from shared/contracts-gen for full type safety. + */ +export interface EscrowEvent { + deposited: { + milestone_index: number + token: string + amount: bigint + depositor: string + } + released: { + milestone_index: number + freelancer: string + amount: bigint + } + refunded: { + milestone_index: number + client: string + amount: bigint + } +} + +/** + * Typed event data for dispute contract events. + * Import from shared/contracts-gen for full type safety. + */ +export interface DisputeEvent { + dispute_opened: { + dispute_id: Uint8Array + opener: string + reason: string + } + evidence_submitted: { + submitter: string + evidence_uri: string + } + vote_cast: { + voter: string + in_favor: boolean + } + dispute_resolved: { + outcome: string + votes_for: number + votes_against: number + } +} + +export type TypedContractEvent = + | { source: 'escrow'; type: keyof EscrowEvent; data: EscrowEvent[keyof EscrowEvent] } + | { source: 'dispute'; type: keyof DisputeEvent; data: DisputeEvent[keyof DisputeEvent] } + export interface ContractEvent { /** Unique + monotonically increasing per contract, used for dedup and as the resume cursor. */ id: string From 806ae1d7ebcd38178e929b1c1f44cb34f1032750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Crhemy-arc=E2=80=9D?= <“rhemaadzer@gmail.com”> Date: Tue, 21 Jul 2026 20:00:28 +0100 Subject: [PATCH 09/11] feat(dispute-event-feed): display typed event data in feed Update DisputeEventFeed to display structured event data using the typed EscrowEvent and DisputeEvent interfaces. Shows relevant details like amounts, reasons, and outcomes for each event type. --- .../molecules/dispute-event-feed/index.tsx | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/components/molecules/dispute-event-feed/index.tsx b/components/molecules/dispute-event-feed/index.tsx index 9249f63..84bdcea 100644 --- a/components/molecules/dispute-event-feed/index.tsx +++ b/components/molecules/dispute-event-feed/index.tsx @@ -1,13 +1,38 @@ import moment from 'moment' import { useContractEvents } from '../../../hooks' import { DISPUTE_CONTRACT_ID, ESCROW_CONTRACT_ID } from '../../../shared/contracts' -import type { ContractEvent } from '../../../shared/contract-events/types' +import type { ContractEvent, EscrowEvent, DisputeEvent } from '../../../shared/contract-events/types' function eventTitle(event: ContractEvent): string { const topLevel = event.topic[0] return typeof topLevel === 'string' ? topLevel : `${event.source} event` } +function formatEventData(event: ContractEvent): string { + if (event.source === 'escrow' && event.data) { + const data = event.data as EscrowEvent[keyof EscrowEvent] + if ('amount' in data) { + return `${data.amount} tokens` + } + if ('freelancer' in data) { + return `to ${data.freelancer}` + } + } + if (event.source === 'dispute' && event.data) { + const data = event.data as DisputeEvent[keyof DisputeEvent] + if ('reason' in data) { + return data.reason + } + if ('evidence_uri' in data) { + return data.evidence_uri + } + if ('outcome' in data) { + return data.outcome + } + } + return '' +} + function truncate(value: string, size = 6): string { return value.length > size * 2 ? `${value.slice(0, size)}…${value.slice(-size)}` : value } @@ -67,6 +92,9 @@ export function DisputeEventFeed() {
  • {eventTitle(event)}

    + {formatEventData(event) && ( +

    {formatEventData(event)}

    + )}

    {event.source} · ledger {event.ledger} · tx {truncate(event.txHash)}

    From d996e172e533848dc1494698d253fd25dad57864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Crhemy-arc=E2=80=9D?= <“rhemaadzer@gmail.com”> Date: Tue, 21 Jul 2026 20:00:59 +0100 Subject: [PATCH 10/11] docs: document contract bindings codegen in README Add Architecture Notes section explaining the codegen pipeline, how to add new contracts, and generated file locations. Update Roadmap to mark Type-Safe Contract Bindings as completed. --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d213dbf..acc9d0a 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,28 @@ Full page sections wired to on-chain data. - The profile page fetches reputation, bio, and past gigs from `GET /api/profile`, which proxies to `PROFILE_API_BASE_URL` when configured and falls back to typed mock data for local/dev environments. - No new runtime dependencies were added for this feature. +### Contract Bindings Codegen + +TrustFlow uses a codegen pipeline to generate fully typed TypeScript client bindings from Soroban contract specs. This ensures all frontend contract calls are compile-time checked and stay in sync with the contract interface. + +**How it works:** + +1. Contract specs are defined as JSON files in `shared/contracts-raw/` +2. Running `npm run codegen` generates TypeScript bindings in `shared/contracts-gen/` +3. The `npm run build` and `npm run dev` commands automatically run codegen + +**Adding a new contract:** + +1. Create a spec file: `shared/contracts-raw/mycontract.spec.json` +2. Run `npm run codegen` +3. Import the generated client: `import { createMycontractContract } from '../shared/contracts-gen'` + +**Generated files:** + +- `shared/contracts-gen/escrow.ts` — Escrow contract types and client +- `shared/contracts-gen/dispute.ts` — Dispute contract types and client +- `shared/contracts-gen/index.ts` — Barrel export + --- ## 🛡️ Security @@ -205,11 +227,11 @@ Full page sections wired to on-chain data. - [x] Gig explorer with search and filtering - [x] Multi-step gig creation wizard - [x] Responsive mobile-first design +- [x] **Type-Safe Contract Bindings**: Codegen pipeline for compile-time checked contract calls ### 🚧 In Progress - [ ] **Wallet Integration (#21)**: Full Freighter API integration -- [ ] **Escrow SDK (#16)**: Connect gig creation to smart contracts - [ ] **Profile Pages (#10)**: On-chain reputation and work history viewer ### 📋 Planned From 431d8bc9017c621d1e32ffcf4ed9270c52c37eaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Crhemy-arc=E2=80=9D?= <“rhemaadzer@gmail.com”> Date: Tue, 21 Jul 2026 20:10:37 +0100 Subject: [PATCH 11/11] fix(codegen): fix TypeScript errors in generated bindings Fix several TypeScript compilation errors: - Use correct Stellar SDK imports (TransactionBuilder, Networks) - Fix xdr import to be value import, not type import - Use correct Address encoding approach - Fix assembleTransaction to work with Transaction objects - Update hooks to use Buffer instead of Uint8Array for compatibility --- components/molecules/deposits/index.tsx | 2 +- .../molecules/dispute-event-feed/index.tsx | 30 +++----- components/molecules/form-pledge/index.tsx | 2 +- hooks/useDisputeContract.ts | 20 +++--- hooks/useEscrowContract.ts | 20 +++--- scripts/generate-contract-bindings.js | 38 +++++----- shared/contracts-gen/dispute.ts | 71 +++++++++++-------- shared/contracts-gen/escrow.ts | 63 +++++++++------- 8 files changed, 129 insertions(+), 117 deletions(-) diff --git a/components/molecules/deposits/index.tsx b/components/molecules/deposits/index.tsx index 8cfcce8..0fa0943 100644 --- a/components/molecules/deposits/index.tsx +++ b/components/molecules/deposits/index.tsx @@ -6,7 +6,7 @@ import { useEscrowContract } from '../../../hooks' export interface IDepositsProps { address: string decimals: number - gigId?: Uint8Array + gigId?: Buffer milestoneIndex?: number name?: string symbol?: string diff --git a/components/molecules/dispute-event-feed/index.tsx b/components/molecules/dispute-event-feed/index.tsx index 84bdcea..089a4a9 100644 --- a/components/molecules/dispute-event-feed/index.tsx +++ b/components/molecules/dispute-event-feed/index.tsx @@ -9,26 +9,18 @@ function eventTitle(event: ContractEvent): string { } function formatEventData(event: ContractEvent): string { - if (event.source === 'escrow' && event.data) { - const data = event.data as EscrowEvent[keyof EscrowEvent] - if ('amount' in data) { - return `${data.amount} tokens` - } - if ('freelancer' in data) { - return `to ${data.freelancer}` - } + if (!event.data || typeof event.data !== 'object') return '' + + const data = event.data as Record + + if (event.source === 'escrow') { + if ('amount' in data) return `${data.amount} tokens` + if ('freelancer' in data) return `to ${data.freelancer}` } - if (event.source === 'dispute' && event.data) { - const data = event.data as DisputeEvent[keyof DisputeEvent] - if ('reason' in data) { - return data.reason - } - if ('evidence_uri' in data) { - return data.evidence_uri - } - if ('outcome' in data) { - return data.outcome - } + if (event.source === 'dispute') { + if ('reason' in data) return String(data.reason) + if ('evidence_uri' in data) return String(data.evidence_uri) + if ('outcome' in data) return String(data.outcome) } return '' } diff --git a/components/molecules/form-pledge/index.tsx b/components/molecules/form-pledge/index.tsx index af7d1f6..30ab3ba 100644 --- a/components/molecules/form-pledge/index.tsx +++ b/components/molecules/form-pledge/index.tsx @@ -10,7 +10,7 @@ export interface IFormPledgeProps { account: string decimals: number symbol?: string - gigId?: Uint8Array + gigId?: Buffer milestoneIndex?: number onPledge: () => void updatedAt: number diff --git a/hooks/useDisputeContract.ts b/hooks/useDisputeContract.ts index a4cc01d..0bc9eaa 100644 --- a/hooks/useDisputeContract.ts +++ b/hooks/useDisputeContract.ts @@ -10,11 +10,11 @@ import { DISPUTE_CONTRACT_ID } from '../shared/contracts' export type { Dispute, DisputeOutcome, DisputeStatus } interface UseDisputeContractResult { - openDispute: (gigId: Uint8Array, milestoneIndex: number, reason: string) => Promise - submitEvidence: (disputeId: Uint8Array, evidenceUri: string) => Promise - vote: (disputeId: Uint8Array, inFavor: boolean) => Promise - resolve: (disputeId: Uint8Array) => Promise - getDispute: (disputeId: Uint8Array) => Promise + openDispute: (gigId: Buffer, milestoneIndex: number, reason: string) => Promise + submitEvidence: (disputeId: Buffer, evidenceUri: string) => Promise + vote: (disputeId: Buffer, inFavor: boolean) => Promise + resolve: (disputeId: Buffer) => Promise + getDispute: (disputeId: Buffer) => Promise isReady: boolean error: string | null clearError: () => void @@ -46,7 +46,7 @@ export function useDisputeContract(): UseDisputeContractResult { ) const openDispute = useCallback( - (gigId: Uint8Array, milestoneIndex: number, reason: string) => { + (gigId: Buffer, milestoneIndex: number, reason: string) => { if (!contract) throw new Error('Dispute contract not configured') return withErrorHandling(() => contract.open_dispute(gigId, milestoneIndex, reason)) }, @@ -54,7 +54,7 @@ export function useDisputeContract(): UseDisputeContractResult { ) const submitEvidence = useCallback( - (disputeId: Uint8Array, evidenceUri: string) => { + (disputeId: Buffer, evidenceUri: string) => { if (!contract) throw new Error('Dispute contract not configured') return withErrorHandling(() => contract.submit_evidence(disputeId, evidenceUri)) }, @@ -62,7 +62,7 @@ export function useDisputeContract(): UseDisputeContractResult { ) const vote = useCallback( - (disputeId: Uint8Array, inFavor: boolean) => { + (disputeId: Buffer, inFavor: boolean) => { if (!contract) throw new Error('Dispute contract not configured') return withErrorHandling(() => contract.vote(disputeId, inFavor)) }, @@ -70,7 +70,7 @@ export function useDisputeContract(): UseDisputeContractResult { ) const resolve = useCallback( - (disputeId: Uint8Array) => { + (disputeId: Buffer) => { if (!contract) throw new Error('Dispute contract not configured') return withErrorHandling(() => contract.resolve(disputeId)) }, @@ -78,7 +78,7 @@ export function useDisputeContract(): UseDisputeContractResult { ) const getDispute = useCallback( - (disputeId: Uint8Array) => { + (disputeId: Buffer) => { if (!contract) throw new Error('Dispute contract not configured') return withErrorHandling(() => contract.get_dispute(disputeId)) }, diff --git a/hooks/useEscrowContract.ts b/hooks/useEscrowContract.ts index 2db7215..65604b6 100644 --- a/hooks/useEscrowContract.ts +++ b/hooks/useEscrowContract.ts @@ -9,11 +9,11 @@ import { ESCROW_CONTRACT_ID } from '../shared/contracts' export type { Milestone, MilestoneStatus } interface UseEscrowContractResult { - deposit: (gigId: Uint8Array, milestoneIndex: number, token: string, amount: bigint) => Promise - release: (gigId: Uint8Array, milestoneIndex: number) => Promise - refund: (gigId: Uint8Array, milestoneIndex: number) => Promise - getBalance: (gigId: Uint8Array, milestoneIndex: number) => Promise - getMilestones: (gigId: Uint8Array) => Promise + deposit: (gigId: Buffer, milestoneIndex: number, token: string, amount: bigint) => Promise + release: (gigId: Buffer, milestoneIndex: number) => Promise + refund: (gigId: Buffer, milestoneIndex: number) => Promise + getBalance: (gigId: Buffer, milestoneIndex: number) => Promise + getMilestones: (gigId: Buffer) => Promise isReady: boolean error: string | null clearError: () => void @@ -45,7 +45,7 @@ export function useEscrowContract(): UseEscrowContractResult { ) const deposit = useCallback( - (gigId: Uint8Array, milestoneIndex: number, token: string, amount: bigint) => { + (gigId: Buffer, milestoneIndex: number, token: string, amount: bigint) => { if (!contract) throw new Error('Escrow contract not configured') return withErrorHandling(() => contract.deposit(gigId, milestoneIndex, token, amount)) }, @@ -53,7 +53,7 @@ export function useEscrowContract(): UseEscrowContractResult { ) const release = useCallback( - (gigId: Uint8Array, milestoneIndex: number) => { + (gigId: Buffer, milestoneIndex: number) => { if (!contract) throw new Error('Escrow contract not configured') return withErrorHandling(() => contract.release(gigId, milestoneIndex)) }, @@ -61,7 +61,7 @@ export function useEscrowContract(): UseEscrowContractResult { ) const refund = useCallback( - (gigId: Uint8Array, milestoneIndex: number) => { + (gigId: Buffer, milestoneIndex: number) => { if (!contract) throw new Error('Escrow contract not configured') return withErrorHandling(() => contract.refund(gigId, milestoneIndex)) }, @@ -69,7 +69,7 @@ export function useEscrowContract(): UseEscrowContractResult { ) const getBalance = useCallback( - (gigId: Uint8Array, milestoneIndex: number) => { + (gigId: Buffer, milestoneIndex: number) => { if (!contract) throw new Error('Escrow contract not configured') return withErrorHandling(() => contract.get_balance(gigId, milestoneIndex)) }, @@ -77,7 +77,7 @@ export function useEscrowContract(): UseEscrowContractResult { ) const getMilestones = useCallback( - (gigId: Uint8Array) => { + (gigId: Buffer) => { if (!contract) throw new Error('Escrow contract not configured') return withErrorHandling(() => contract.get_gig_milestones(gigId)) }, diff --git a/scripts/generate-contract-bindings.js b/scripts/generate-contract-bindings.js index db99e69..86d9db2 100644 --- a/scripts/generate-contract-bindings.js +++ b/scripts/generate-contract-bindings.js @@ -23,7 +23,7 @@ const TYPE_MAP = { I32: 'number', I128: 'bigint', String: 'string', - Bytes: 'Uint8Array', + Bytes: 'Buffer', Address: 'string', } @@ -32,10 +32,8 @@ function mapType(typeDef, localTypes) { const raw = typeDef.type - // Check if it's a native Soroban type if (TYPE_MAP[raw]) return TYPE_MAP[raw] - // Check for generic types: Vec, Option, Map const vecMatch = raw.match(/^Vec<(.+)>$/) if (vecMatch) { return `${mapType({ type: vecMatch[1] }, localTypes)}[]` @@ -48,10 +46,9 @@ function mapType(typeDef, localTypes) { const bytesNMatch = raw.match(/^BytesN<(\d+)>$/) if (bytesNMatch) { - return 'Uint8Array' + return 'Buffer' } - // Check if it's a local custom type if (localTypes && localTypes[raw]) { return raw } @@ -61,8 +58,7 @@ function mapType(typeDef, localTypes) { function generateTypeInterface(name, typeDef, localTypes) { if (typeDef.variants) { - // Generate an enum/union type - const variants = typeDef.variants.map(v => ` ${v}`).join(' |\n') + const variants = typeDef.variants.map(v => ` '${v}'`).join(' |\n') return `export type ${name} =\n${variants}\n` } @@ -107,7 +103,7 @@ function generateBindings(spec) { lines.push(` * Auto-generated ${contractName} contract bindings`) lines.push(` * Generated from Soroban contract spec — do not edit manually`) lines.push(` */`) - lines.push(`import { Contract, rpc, xdr, Address } from '@stellar/stellar-sdk'`) + lines.push(`import { Contract, rpc, xdr, TransactionBuilder, Networks } from '@stellar/stellar-sdk'`) lines.push(`import { getSorobanServer } from '../soroban-rpc'`) lines.push(`import { ${spec.contract.toUpperCase()}_CONTRACT_ID } from '../contracts'`) lines.push(``) @@ -149,15 +145,24 @@ function generateBindings(spec) { lines.push(` const contract = new Contract(contractId)`) lines.push(``) lines.push(` async function invoke(method: string, ...args: xdr.ScVal[]): Promise {`) - lines.push(` const tx = await server.simulateTransaction(contract.call(method, ...args))`) - lines.push(` const result = rpc.assembleTransaction(contract.call(method, ...args), tx).toXDR()`) - lines.push(` const sendResponse = await server.sendTransaction(result)`) + lines.push(` const sourceAccount = await server.getAccount(contractId)`) + lines.push(` const builtTx = new TransactionBuilder(sourceAccount, {`) + lines.push(` fee: '100',`) + lines.push(` networkPassphrase: Networks.TESTNET,`) + lines.push(` })`) + lines.push(` .addOperation(contract.call(method, ...args))`) + lines.push(` .setTimeout(30)`) + lines.push(` .build()`) + lines.push(` const simulation = await server.simulateTransaction(builtTx)`) + lines.push(` const assembledTx = rpc.assembleTransaction(builtTx, simulation)`) + lines.push(` const sendResponse = await server.sendTransaction(assembledTx.build())`) lines.push(` const resultResponse = await server.getTransaction(sendResponse.hash)`) lines.push(` if (resultResponse.status !== 'SUCCESS') {`) lines.push(` throw new Error(\`Transaction failed: \${resultResponse.status}\`)`) lines.push(` }`) - lines.push(` const returnValue = contract.call(method, ...args).result?.val`) - lines.push(` return rpc.scValToNative(returnValue as xdr.ScVal) as T`) + lines.push(` const retval = (resultResponse as any).result?.retval`) + lines.push(` if (!retval) return undefined as T`) + lines.push(` return retval as T`) lines.push(` }`) lines.push(``) lines.push(` return {`) @@ -173,13 +178,13 @@ function generateBindings(spec) { const scVals = Object.entries(methodDef.args || {}) .map(([name, def]) => { - if (def.type === 'Address') return `new Address(${name}).toScVal()` + if (def.type === 'Address') return `xdr.ScVal.scvString(${name})` if (def.type === 'I128') return `xdr.ScVal.scvI128(new xdr.Int128Parts({ hi: xdr.Int64.fromString(String(BigInt(${name}) >> 64n)), lo: xdr.Int64.fromString(String(BigInt(${name}) & 0xFFFFFFFFFFFFFFFFn)) }))` if (def.type === 'U32') return `xdr.ScVal.scvU32(${name})` if (def.type === 'U64') return `xdr.ScVal.scvU64(xdr.Int64.fromString(String(${name})))` if (def.type === 'Bool') return `xdr.ScVal.scvBool(${name})` if (def.type === 'String') return `xdr.ScVal.scvString(${name})` - if (def.type === 'BytesN<32>') return `xdr.ScVal.scvBytes(Uint8Array.from(${name}))` + if (def.type === 'BytesN<32>') return `xdr.ScVal.scvBytes(${name})` return `xdr.ScVal.scvBytes(${name})` }) .join(', ') @@ -210,7 +215,6 @@ function main() { process.exit(1) } - // Ensure output directory exists if (!fs.existsSync(OUTPUT_DIR)) { fs.mkdirSync(OUTPUT_DIR, { recursive: true }) } @@ -225,7 +229,6 @@ function main() { for (const specFile of specFiles) { const specPath = path.join(SPECS_DIR, specFile) const spec = JSON.parse(fs.readFileSync(specPath, 'utf-8')) - const contractName = spec.contract.charAt(0).toUpperCase() + spec.contract.slice(1) const output = generateBindings(spec) const outputPath = path.join(OUTPUT_DIR, `${spec.contract}.ts`) @@ -235,7 +238,6 @@ function main() { indexLines.push(`export * from './${spec.contract}'`) } - // Generate barrel export const indexPath = path.join(OUTPUT_DIR, 'index.ts') fs.writeFileSync(indexPath, indexLines.join('\n'), 'utf-8') console.log(`Generated: ${indexPath}`) diff --git a/shared/contracts-gen/dispute.ts b/shared/contracts-gen/dispute.ts index af40f4d..f785ba6 100644 --- a/shared/contracts-gen/dispute.ts +++ b/shared/contracts-gen/dispute.ts @@ -2,15 +2,15 @@ * Auto-generated Dispute contract bindings * Generated from Soroban contract spec — do not edit manually */ -import { Contract, rpc, xdr, Address } from '@stellar/stellar-sdk' +import { Contract, rpc, xdr, TransactionBuilder, Networks } from '@stellar/stellar-sdk' import { getSorobanServer } from '../soroban-rpc' import { DISPUTE_CONTRACT_ID } from '../contracts' // ── Types ────────────────────────────────────────────────────── export interface Dispute { - id: Uint8Array - gig_id: Uint8Array + id: Buffer + gig_id: Buffer milestone_index: number opener: string reason: string @@ -22,20 +22,20 @@ export interface Dispute { } export type DisputeStatus = - Open | - Voting | - Resolved | - Executed + 'Open' | + 'Voting' | + 'Resolved' | + 'Executed' export type DisputeOutcome = - FreelancerWins | - ClientWins | - Split + 'FreelancerWins' | + 'ClientWins' | + 'Split' // ── Event Data Types ─────────────────────────────────────────── export interface DisputeDispute_openedEventData { - dispute_id: Uint8Array + dispute_id: Buffer opener: string reason: string } @@ -60,15 +60,15 @@ export interface DisputeDispute_resolvedEventData { export interface IDisputeContract { /** Open a dispute for a gig milestone */ - open_dispute(gig_id: Uint8Array, milestone_index: number, reason: string): Promise + open_dispute(gig_id: Buffer, milestone_index: number, reason: string): Promise /** Submit evidence for a dispute */ - submit_evidence(dispute_id: Uint8Array, evidence_uri: string): Promise + submit_evidence(dispute_id: Buffer, evidence_uri: string): Promise /** Cast a juror vote on a dispute */ - vote(dispute_id: Uint8Array, in_favor: boolean): Promise + vote(dispute_id: Buffer, in_favor: boolean): Promise /** Resolve a dispute and execute the outcome */ - resolve(dispute_id: Uint8Array): Promise + resolve(dispute_id: Buffer): Promise /** Get dispute details */ - get_dispute(dispute_id: Uint8Array): Promise + get_dispute(dispute_id: Buffer): Promise } // ── Contract Client ───────────────────────────────────────────── @@ -79,50 +79,59 @@ export function createDisputeContract(): IDisputeContract { const contract = new Contract(contractId) async function invoke(method: string, ...args: xdr.ScVal[]): Promise { - const tx = await server.simulateTransaction(contract.call(method, ...args)) - const result = rpc.assembleTransaction(contract.call(method, ...args), tx).toXDR() - const sendResponse = await server.sendTransaction(result) + const sourceAccount = await server.getAccount(contractId) + const builtTx = new TransactionBuilder(sourceAccount, { + fee: '100', + networkPassphrase: Networks.TESTNET, + }) + .addOperation(contract.call(method, ...args)) + .setTimeout(30) + .build() + const simulation = await server.simulateTransaction(builtTx) + const assembledTx = rpc.assembleTransaction(builtTx, simulation) + const sendResponse = await server.sendTransaction(assembledTx.build()) const resultResponse = await server.getTransaction(sendResponse.hash) if (resultResponse.status !== 'SUCCESS') { throw new Error(`Transaction failed: ${resultResponse.status}`) } - const returnValue = contract.call(method, ...args).result?.val - return rpc.scValToNative(returnValue as xdr.ScVal) as T + const retval = (resultResponse as any).result?.retval + if (!retval) return undefined as T + return retval as T } return { - open_dispute: (gig_id: Uint8Array, milestone_index: number, reason: string): Promise => { + open_dispute: (gig_id: Buffer, milestone_index: number, reason: string): Promise => { return invoke( 'open_dispute', - xdr.ScVal.scvBytes(Uint8Array.from(gig_id)), xdr.ScVal.scvU32(milestone_index), xdr.ScVal.scvString(reason) + xdr.ScVal.scvBytes(gig_id), xdr.ScVal.scvU32(milestone_index), xdr.ScVal.scvString(reason) ) }, - submit_evidence: (dispute_id: Uint8Array, evidence_uri: string): Promise => { + submit_evidence: (dispute_id: Buffer, evidence_uri: string): Promise => { return invoke( 'submit_evidence', - xdr.ScVal.scvBytes(Uint8Array.from(dispute_id)), xdr.ScVal.scvString(evidence_uri) + xdr.ScVal.scvBytes(dispute_id), xdr.ScVal.scvString(evidence_uri) ) }, - vote: (dispute_id: Uint8Array, in_favor: boolean): Promise => { + vote: (dispute_id: Buffer, in_favor: boolean): Promise => { return invoke( 'vote', - xdr.ScVal.scvBytes(Uint8Array.from(dispute_id)), xdr.ScVal.scvBool(in_favor) + xdr.ScVal.scvBytes(dispute_id), xdr.ScVal.scvBool(in_favor) ) }, - resolve: (dispute_id: Uint8Array): Promise => { + resolve: (dispute_id: Buffer): Promise => { return invoke( 'resolve', - xdr.ScVal.scvBytes(Uint8Array.from(dispute_id)) + xdr.ScVal.scvBytes(dispute_id) ) }, - get_dispute: (dispute_id: Uint8Array): Promise => { + get_dispute: (dispute_id: Buffer): Promise => { return invoke( 'get_dispute', - xdr.ScVal.scvBytes(Uint8Array.from(dispute_id)) + xdr.ScVal.scvBytes(dispute_id) ) }, diff --git a/shared/contracts-gen/escrow.ts b/shared/contracts-gen/escrow.ts index fb8ee7b..8077568 100644 --- a/shared/contracts-gen/escrow.ts +++ b/shared/contracts-gen/escrow.ts @@ -2,7 +2,7 @@ * Auto-generated Escrow contract bindings * Generated from Soroban contract spec — do not edit manually */ -import { Contract, rpc, xdr, Address } from '@stellar/stellar-sdk' +import { Contract, rpc, xdr, TransactionBuilder, Networks } from '@stellar/stellar-sdk' import { getSorobanServer } from '../soroban-rpc' import { ESCROW_CONTRACT_ID } from '../contracts' @@ -17,12 +17,12 @@ export interface Milestone { } export type MilestoneStatus = - Pending | - Funded | - Completed | - Released | - Disputed | - Refunded + 'Pending' | + 'Funded' | + 'Completed' | + 'Released' | + 'Disputed' | + 'Refunded' // ── Event Data Types ─────────────────────────────────────────── @@ -49,15 +49,15 @@ export interface EscrowRefundedEventData { export interface IEscrowContract { /** Deposit funds into escrow for a gig milestone */ - deposit(gig_id: Uint8Array, milestone_index: number, token: string, amount: bigint): Promise + deposit(gig_id: Buffer, milestone_index: number, token: string, amount: bigint): Promise /** Release escrowed funds to the freelancer */ - release(gig_id: Uint8Array, milestone_index: number): Promise + release(gig_id: Buffer, milestone_index: number): Promise /** Refund escrowed funds to the client */ - refund(gig_id: Uint8Array, milestone_index: number): Promise + refund(gig_id: Buffer, milestone_index: number): Promise /** Get the escrowed balance for a milestone */ - get_balance(gig_id: Uint8Array, milestone_index: number): Promise + get_balance(gig_id: Buffer, milestone_index: number): Promise /** Get all milestones for a gig */ - get_gig_milestones(gig_id: Uint8Array): Promise + get_gig_milestones(gig_id: Buffer): Promise } // ── Contract Client ───────────────────────────────────────────── @@ -68,50 +68,59 @@ export function createEscrowContract(): IEscrowContract { const contract = new Contract(contractId) async function invoke(method: string, ...args: xdr.ScVal[]): Promise { - const tx = await server.simulateTransaction(contract.call(method, ...args)) - const result = rpc.assembleTransaction(contract.call(method, ...args), tx).toXDR() - const sendResponse = await server.sendTransaction(result) + const sourceAccount = await server.getAccount(contractId) + const builtTx = new TransactionBuilder(sourceAccount, { + fee: '100', + networkPassphrase: Networks.TESTNET, + }) + .addOperation(contract.call(method, ...args)) + .setTimeout(30) + .build() + const simulation = await server.simulateTransaction(builtTx) + const assembledTx = rpc.assembleTransaction(builtTx, simulation) + const sendResponse = await server.sendTransaction(assembledTx.build()) const resultResponse = await server.getTransaction(sendResponse.hash) if (resultResponse.status !== 'SUCCESS') { throw new Error(`Transaction failed: ${resultResponse.status}`) } - const returnValue = contract.call(method, ...args).result?.val - return rpc.scValToNative(returnValue as xdr.ScVal) as T + const retval = (resultResponse as any).result?.retval + if (!retval) return undefined as T + return retval as T } return { - deposit: (gig_id: Uint8Array, milestone_index: number, token: string, amount: bigint): Promise => { + deposit: (gig_id: Buffer, milestone_index: number, token: string, amount: bigint): Promise => { return invoke( 'deposit', - xdr.ScVal.scvBytes(Uint8Array.from(gig_id)), xdr.ScVal.scvU32(milestone_index), new Address(token).toScVal(), xdr.ScVal.scvI128(new xdr.Int128Parts({ hi: xdr.Int64.fromString(String(BigInt(amount) >> 64n)), lo: xdr.Int64.fromString(String(BigInt(amount) & 0xFFFFFFFFFFFFFFFFn)) })) + xdr.ScVal.scvBytes(gig_id), xdr.ScVal.scvU32(milestone_index), xdr.ScVal.scvString(token), xdr.ScVal.scvI128(new xdr.Int128Parts({ hi: xdr.Int64.fromString(String(BigInt(amount) >> 64n)), lo: xdr.Int64.fromString(String(BigInt(amount) & 0xFFFFFFFFFFFFFFFFn)) })) ) }, - release: (gig_id: Uint8Array, milestone_index: number): Promise => { + release: (gig_id: Buffer, milestone_index: number): Promise => { return invoke( 'release', - xdr.ScVal.scvBytes(Uint8Array.from(gig_id)), xdr.ScVal.scvU32(milestone_index) + xdr.ScVal.scvBytes(gig_id), xdr.ScVal.scvU32(milestone_index) ) }, - refund: (gig_id: Uint8Array, milestone_index: number): Promise => { + refund: (gig_id: Buffer, milestone_index: number): Promise => { return invoke( 'refund', - xdr.ScVal.scvBytes(Uint8Array.from(gig_id)), xdr.ScVal.scvU32(milestone_index) + xdr.ScVal.scvBytes(gig_id), xdr.ScVal.scvU32(milestone_index) ) }, - get_balance: (gig_id: Uint8Array, milestone_index: number): Promise => { + get_balance: (gig_id: Buffer, milestone_index: number): Promise => { return invoke( 'get_balance', - xdr.ScVal.scvBytes(Uint8Array.from(gig_id)), xdr.ScVal.scvU32(milestone_index) + xdr.ScVal.scvBytes(gig_id), xdr.ScVal.scvU32(milestone_index) ) }, - get_gig_milestones: (gig_id: Uint8Array): Promise => { + get_gig_milestones: (gig_id: Buffer): Promise => { return invoke( 'get_gig_milestones', - xdr.ScVal.scvBytes(Uint8Array.from(gig_id)) + xdr.ScVal.scvBytes(gig_id) ) },