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
62 changes: 62 additions & 0 deletions agent-context/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -638,3 +638,65 @@ Acceptance notes:
- `pnpm validate` passes.
- Public boundary scan passes and finds no private backend, Cubid internal, wallet graph, service-role, private storage, or deployment internals.
- Staged smoke checklist references the corrected SDK contracts for Cubid SDK, MyPayTag backend, one test PayingDapp, one test PayToDapp, and SmarTrust swap/bridge NEAR 1Click.

## Sprint 8: Code-Verified Backend Compatibility Closure

### GPTS-S8-T1 Add Backend Compatibility Fixtures For Public Paytag Boundaries

Status: Complete
Feature branch: codex/mypaytag-mvp-code-gap-todos-20260629
Session log: agent-context/session-log/main.md#2026-06-29-gpts-s8-t1
Depends on: mypaytag:GPTR-S8-T1, smartrust-wallet:STW-S28-T1

Add fixtures and conformance tests that prove the SDK's canonical
`identifierType: "paytag"` resolve, route registration, provider callback,
notification, and intent payloads are the payloads the backend accepts and
returns.

Acceptance notes:

- Tests fail if SDK fixtures or generated types reintroduce public
`identifierType: "verified_stamp"`.
- Fixtures include SmarTrust PayingDapp resolve and SmarTrust PayToDapp route
registration examples.
- Backend response examples validate through SDK schemas without compatibility
casts.
- The staged smoke checklist names this as the contract gate before hosted
staging smoke.

### GPTS-S8-T2 Add Public NEAR 1Click Endpoint Client Helpers

Status: Complete
Feature branch: codex/mypaytag-mvp-code-gap-todos-20260629
Session log: agent-context/session-log/main.md#2026-06-29-gpts-s8-t2
Depends on: mypaytag:GPTR-S8-T2, mypaytag-sdk:GPTS-S7-T1, mypaytag-sdk:GPTS-S7-T2

Add SDK helpers for calling the backend NEAR 1Click MVP quote and selected-quote
payable-instruction endpoints once those endpoints are exposed.

Acceptance notes:

- Helpers build and validate quote requests, quote options, selected-quote
requests, and payable instructions using the existing NEAR schemas.
- Helpers do not expose LI.FI, Squid, 0x, Across, LayerZero/Stargate, broad
fanout, or generic external adapter support as MVP dependencies.
- Tests cover success and safe failure parsing for quote unavailable, quote
expired, route revoked, authorization required, and invalid response.

### GPTS-S8-T3 Guard Phase 2 Solver Helpers Against MVP Import Drift

Status: Complete
Feature branch: codex/mypaytag-mvp-code-gap-todos-20260629
Session log: agent-context/session-log/main.md#2026-06-29-gpts-s8-t3
Depends on: mypaytag-sdk:GPTS-S8-T2

Add a package-level guardrail proving MVP examples and helpers do not require
the Phase 2 `requestExecutionQuotes` fanout helper.

Acceptance notes:

- MVP examples import resolve, provider callback, hosted action, route CRUD,
and NEAR 1Click helpers only.
- Phase 2 solver helper tests remain labeled as non-MVP.
- Validation fails if Phase 2 solver ids appear in MVP happy-path fixtures as
active execution requirements.
102 changes: 102 additions & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,37 @@ export type SupportedPath = ResolveRequest["supportedPaths"][number];
export type ResolveAmount = ResolveRequest["amount"];
export type PayorAmountExactness = "exact_send" | "exact_receive";

export interface NearOneClickReceiveRequirement {
destinationAsset: string;
recipient: string;
amount: string;
expiresAt: string;
resolverReference: string;
}

export interface NearOneClickQuoteEndpointRequest {
schema?: "mypaytag.near_1click.quote_request.v1";
receiveRequirement: NearOneClickReceiveRequirement;
selectedRouteReference?: string;
sourceAsset: string;
sourceAmount: string;
payorReference: string;
}

export interface NearOneClickQuoteEndpointResponse {
status: "quoted";
adapter: "near_intents_1click";
resolverReference: string;
selectedRouteReference: string;
quotes: NearOneClickMvpQuoteOption[];
}

export interface MyPayTagEndpointCallOptions {
endpoint: string;
fetch?: typeof fetch;
headers?: Record<string, string>;
}

export interface PayorAppReferenceInput {
payorAppId: string;
reference: string;
Expand Down Expand Up @@ -176,6 +207,21 @@ export function parseNearOneClickPayableInstruction(
return validateNearOneClickPayableInstruction(payload);
}

export async function requestNearOneClickQuoteOptions(
options: MyPayTagEndpointCallOptions & { request: NearOneClickQuoteEndpointRequest },
): Promise<NearOneClickQuoteEndpointResponse> {
assertNearOneClickQuoteRequest(options.request);
const payload = await postJson(options.endpoint, options.request, options);
return parseNearOneClickQuoteEndpointResponse(payload);
}

export async function selectNearOneClickQuote(
options: MyPayTagEndpointCallOptions & { request: NearOneClickMvpQuoteSelectionRequest },
): Promise<NearOneClickMvpPayableInstruction> {
const request = buildNearOneClickQuoteSelectionRequest(options.request);
return parseNearOneClickPayableInstruction(await postJson(options.endpoint, request, options));
}

export function isMyPayTagNotification(payload: unknown): payload is NotificationEvent {
return isNotificationEvent(payload);
}
Expand Down Expand Up @@ -218,6 +264,62 @@ function requireNonEmpty(value: string, fieldName: string): string {
return trimmed;
}

function parseNearOneClickQuoteEndpointResponse(payload: unknown): NearOneClickQuoteEndpointResponse {
if (!payload || typeof payload !== "object") {
throw new Error("invalid_near_1click_quote_response");
}
const candidate = payload as NearOneClickQuoteEndpointResponse;
if (
candidate.status !== "quoted" ||
candidate.adapter !== "near_intents_1click" ||
!candidate.resolverReference ||
!candidate.selectedRouteReference ||
!Array.isArray(candidate.quotes)
) {
throw new Error("invalid_near_1click_quote_response");
}

return {
...candidate,
quotes: candidate.quotes.map(parseNearOneClickQuoteOption),
};
}

function assertNearOneClickQuoteRequest(request: NearOneClickQuoteEndpointRequest): void {
if (!request.sourceAsset || !request.sourceAmount || !request.payorReference) {
throw new Error("invalid_near_1click_quote_request");
}
if (
!request.receiveRequirement?.destinationAsset ||
!request.receiveRequirement.recipient ||
!request.receiveRequirement.amount ||
!request.receiveRequirement.expiresAt ||
!request.receiveRequirement.resolverReference
) {
throw new Error("invalid_near_1click_quote_request");
}
}

async function postJson(
endpoint: string,
body: unknown,
options: Pick<MyPayTagEndpointCallOptions, "fetch" | "headers"> = {},
): Promise<unknown> {
const requestFetch = options.fetch ?? globalThis.fetch;
const response = await requestFetch(endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
...(options.headers ?? {}),
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`mypaytag_endpoint_error:${response.status}`);
}
return await response.json();
}

/**
* Requests Phase 2 non-MVP execution quotes from caller-provided providers.
* Core MyPayTag MVP integrations can resolve paytags, parse NEAR 1Click quote
Expand Down
88 changes: 88 additions & 0 deletions packages/sdk/src/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
validResolvedResponse,
validResolveRequest,
validRouteSelectionResponse,
validProviderCallbackRequest,
validRouteRegistrationRequest,
} from "@mypaytag/protocol";

import {
Expand All @@ -18,13 +20,16 @@ import {
buildPayorAppReference,
buildPayorAppResolveRequest,
buildSupportedPath,
cryptoNativeExecutionSolvers,
getActionUrl,
isActionRequired,
isMyPayTagNotification,
isResolved,
parseNotificationEvent,
parseResolveResponse,
requestNearOneClickQuoteOptions,
requestExecutionQuotes,
selectNearOneClickQuote,
parseNearOneClickPayableInstruction,
parseNearOneClickQuoteOption,
type CryptoNativeExecutionSolverId,
Expand All @@ -36,6 +41,14 @@ describe("@mypaytag/sdk", () => {
expect(buildResolveRequest(validResolveRequest)).toEqual(validResolveRequest);
});

it("keeps public MVP payloads on the paytag identifier contract", () => {
expect(validResolveRequest.recipient.identifierType).toBe("paytag");
expect(validResolvedResponse.intent.recipient.identifierType).toBe("paytag");
expect(validNotificationEvent.recipient.identifierType).toBe("paytag");
expect(validRouteRegistrationRequest.recipient.identifierType).toBe("paytag");
expect(validProviderCallbackRequest.recipient.identifierType).toBe("paytag");
});

it("builds generic payor-app resolve request inputs", () => {
const supportedPath = buildSupportedPath({
chain: " base ",
Expand Down Expand Up @@ -146,6 +159,74 @@ describe("@mypaytag/sdk", () => {
);
});

it("calls NEAR 1Click quote and selected-quote backend endpoints", async () => {
const calls: Array<{ endpoint: string; body: unknown }> = [];
const fakeFetch: typeof fetch = async (input, init) => {
calls.push({
endpoint: String(input),
body: JSON.parse(String(init?.body)),
});
if (String(input).endsWith("/near-oneclick-quotes")) {
return jsonResponse({
status: "quoted",
adapter: "near_intents_1click",
resolverReference: validNearOneClickQuoteOption.resolverReference,
selectedRouteReference: validNearOneClickQuoteOption.selectedRouteReference,
quotes: [validNearOneClickQuoteOption],
});
}
return jsonResponse(validNearOneClickPayableInstruction);
};

const quotes = await requestNearOneClickQuoteOptions({
endpoint: "https://resolver.test/functions/v1/near-oneclick-quotes",
fetch: fakeFetch,
headers: { "x-mypaytag-dapp-id": "smartrust-wallet" },
request: {
sourceAsset: "near/mainnet/USDC",
sourceAmount: "25.18",
payorReference: "smartrust:send_001",
receiveRequirement: {
destinationAsset: "base/mainnet/USDC",
recipient: validNearOneClickQuoteOption.selectedRouteReference,
amount: "25.00",
expiresAt: validNearOneClickQuoteOption.expiresAt,
resolverReference: validNearOneClickQuoteOption.resolverReference,
},
},
});
const instruction = await selectNearOneClickQuote({
endpoint: "https://resolver.test/functions/v1/near-oneclick-selected-quote",
fetch: fakeFetch,
request: validNearOneClickQuoteSelectionRequest,
});

expect(quotes.quotes).toEqual([validNearOneClickQuoteOption]);
expect(instruction).toEqual(validNearOneClickPayableInstruction);
expect(calls.map((call) => call.endpoint)).toEqual([
"https://resolver.test/functions/v1/near-oneclick-quotes",
"https://resolver.test/functions/v1/near-oneclick-selected-quote",
]);
expect(calls[1].body).toEqual(validNearOneClickQuoteSelectionRequest);
});

it("keeps Phase 2 solver ids out of MVP happy-path fixtures", () => {
const mvpFixtures = JSON.stringify([
validResolveRequest,
validResolvedResponse,
validRouteRegistrationRequest,
validProviderCallbackRequest,
validNearOneClickQuoteOption,
validNearOneClickQuoteSelectionRequest,
validNearOneClickPayableInstruction,
]);

expect(validNearOneClickQuoteOption.adapter).toBe("near_intents_1click");
for (const solverId of cryptoNativeExecutionSolvers) {
expect(mvpFixtures).not.toContain(solverId);
}
});

it("requests Phase 2 extension quotes from every configured solver when none is preferred", async () => {
const calls: CryptoNativeExecutionSolverId[] = [];
const providers = [
Expand Down Expand Up @@ -236,3 +317,10 @@ function createQuoteProvider(
},
};
}

function jsonResponse(body: unknown): Response {
return new Response(JSON.stringify(body), {
status: 200,
headers: { "content-type": "application/json" },
});
}
Loading