Skip to content
Open
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
11 changes: 10 additions & 1 deletion src/contract/build.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Address, nativeToScVal } from '@stellar/stellar-sdk';
import type { CreateEscrowParams } from '../types';
import type { CreateEscrowParams, ReleaseMilestoneParams } from '../types';

export function buildCreateEscrowArgs(params: CreateEscrowParams): unknown[] {
return [
Expand All @@ -14,6 +14,15 @@ export function buildReleaseArgs(escrowId: string, caller: string): unknown[] {
return [nativeToScVal(escrowId, { type: 'string' }), new Address(caller).toScVal()];
}

export function buildReleaseMilestoneArgs(params: ReleaseMilestoneParams): unknown[] {
return [
nativeToScVal(params.escrowId, { type: 'string' }),
nativeToScVal(params.milestoneId, { type: 'u32' }),
nativeToScVal(params.amountStroops, { type: 'i128' }),
new Address(params.caller).toScVal(),
];
}

export function buildDisputeArgs(escrowId: string, reason: string): unknown[] {
return [nativeToScVal(escrowId, { type: 'string' }), nativeToScVal(reason, { type: 'string' })];
}
1 change: 1 addition & 0 deletions src/escrow/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { EscrowBuilder } from './builder';
export { EscrowMonitor } from './monitor';
export { DisputeClient } from './dispute';
export { MultiSigEscrowClient } from './multisig';
export { releaseEscrow, releaseMilestone } from './release';
33 changes: 32 additions & 1 deletion src/escrow/release.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { TrustFlowClient } from '../client';
import type { ReleaseEscrowParams } from '../types/index';
import type { ReleaseEscrowParams, ReleaseMilestoneParams } from '../types';
import { TrustFlowError } from '../errors';
import { buildReleaseMilestoneArgs } from '../contract/build';

export async function releaseEscrow(
client: TrustFlowClient,
params: ReleaseEscrowParams,
): Promise<string> {
void client;
if (!params.escrowId) {
throw TrustFlowError.validation('escrowId', 'Required');
}
Expand All @@ -16,3 +18,32 @@ export async function releaseEscrow(
// Returns transaction hash
return `tx_release_${params.escrowId}_${Date.now()}`;
}

/**
* Builds the contract arguments for approving and releasing a single escrow
* milestone payment.
*
* The wrapper targets the Soroban `release_milestone` entrypoint with the
* argument order `(escrow_id, milestone_id, amount_stroops, caller)`.
*/
export async function releaseMilestone(
client: TrustFlowClient,
params: ReleaseMilestoneParams,
): Promise<string> {
void client;
if (!params.escrowId) {
throw TrustFlowError.validation('escrowId', 'Required');
}
if (!Number.isInteger(params.milestoneId) || params.milestoneId < 0) {
throw TrustFlowError.validation('milestoneId', 'Must be a non-negative integer');
}
if (params.amountStroops <= 0n) {
throw TrustFlowError.validation('amountStroops', 'Must be greater than zero');
}
if (!params.caller) {
throw TrustFlowError.unauthorized('release milestone');
}

buildReleaseMilestoneArgs(params);
return `tx_release_milestone_${params.escrowId}_${params.milestoneId}_${Date.now()}`;
}
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ export interface ReleaseEscrowParams {
caller: string;
}

export interface ReleaseMilestoneParams {
escrowId: string;
milestoneId: number;
amountStroops: bigint;
caller: string;
}

export interface DisputeEscrowParams {
escrowId: string;
caller: string;
Expand Down
76 changes: 76 additions & 0 deletions tests/escrow.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { EscrowBuilder } from '../src/escrow/builder';
import { nativeToScVal, Address, Keypair } from '@stellar/stellar-sdk';
import { buildReleaseMilestoneArgs } from '../src/contract/build';
import { releaseMilestone } from '../src/escrow/release';
import { TrustFlowClient } from '../src/client';

describe('EscrowBuilder', () => {
const ADDR_A = 'G' + 'A'.repeat(55);
Expand All @@ -19,3 +23,75 @@ describe('EscrowBuilder', () => {
expect(p.deadlineBlocks).toBe(1000);
});
});

describe('releaseMilestone', () => {
const CALLER = Keypair.random().publicKey();
const client = new TrustFlowClient({
contractId: 'C' + 'D'.repeat(55),
network: 'TESTNET',
});

it('builds release_milestone contract args in the expected XDR order', () => {
const args = buildReleaseMilestoneArgs({
escrowId: 'esc-42',
milestoneId: 3,
amountStroops: 25_000_000n,
caller: CALLER,
});

const expected = [
nativeToScVal('esc-42', { type: 'string' }),
nativeToScVal(3, { type: 'u32' }),
nativeToScVal(25_000_000n, { type: 'i128' }),
new Address(CALLER).toScVal(),
];

expect(args.map((arg) => (arg as { toXDR: (format: 'base64') => string }).toXDR('base64'))).toEqual(
expected.map((arg) => arg.toXDR('base64')),
);
});

it('returns a milestone release transaction placeholder for valid input', async () => {
jest.spyOn(Date, 'now').mockReturnValue(123);

await expect(
releaseMilestone(client, {
escrowId: 'esc-42',
milestoneId: 3,
amountStroops: 25_000_000n,
caller: CALLER,
}),
).resolves.toBe('tx_release_milestone_esc-42_3_123');

jest.restoreAllMocks();
});

it('rejects invalid milestone release params', async () => {
await expect(
releaseMilestone(client, {
escrowId: '',
milestoneId: 0,
amountStroops: 1n,
caller: CALLER,
}),
).rejects.toThrow('escrowId');

await expect(
releaseMilestone(client, {
escrowId: 'esc-42',
milestoneId: -1,
amountStroops: 1n,
caller: CALLER,
}),
).rejects.toThrow('milestoneId');

await expect(
releaseMilestone(client, {
escrowId: 'esc-42',
milestoneId: 1,
amountStroops: 0n,
caller: CALLER,
}),
).rejects.toThrow('amountStroops');
});
});