Skip to main content

FPSF-SS-002 — Guides

Layer: Guides · Audience: wallet developers For normative requirements, see the Formal Specification.


Guide 1: Connect and Authenticate

Prerequisites: read Core Concepts — specifically the connection model and message authentication sections.

Step 1 — Open the WebSocket Connection

const ws = new WebSocket('wss://gateway.your-processor.example/ws');

ws.addEventListener('open', () => {
// Send first signed message within the authentication timeout
sendGetBalance(ws, signer, [TOKEN_DOMAIN_SEPARATOR]);
});

ws.addEventListener('message', (event) => {
handleMessage(JSON.parse(event.data));
});

ws.addEventListener('close', (event) => {
console.warn(`Closed: ${event.code} ${event.reason}`);
scheduleReconnect();
});

Step 2 — Build a Signed Message

import { ethers } from 'ethers';

async function buildSignedMessage(signer, type, payload) {
const callerAddress = await signer.getAddress();
const deadline = Math.floor(Date.now() / 1000) + 300; // 5 minutes

const domain = {
name: 'WalletGateway',
version: '1',
chainId: YOUR_CHAIN_ID,
verifyingContract: GATEWAY_ADDRESS, // obtain from basic-data-server
};

const messageTypes = {
GatewayMessage: [
{ name: 'type', type: 'string' },
{ name: 'callerAddress', type: 'address' },
{ name: 'deadline', type: 'uint256' },
{ name: 'payloadHash', type: 'bytes32' },
],
};

const payloadHash = ethers.keccak256(ethers.toUtf8Bytes(JSON.stringify(payload)));
const messageData = { type, callerAddress, deadline, payloadHash };

const sig = await signer.signTypedData(domain, messageTypes, messageData);
const { v, r, s } = ethers.Signature.from(sig);
const hash = ethers.TypedDataEncoder.hash(domain, messageTypes, messageData);

return {
type, callerAddress, deadline, payload,
signature: { hash, v: v < 27 ? v + 27 : v, r, s },
};
}

Step 3 — Handle Responses

function handleMessage(msg) {
switch (msg.type) {
case 'BALANCE_RESULT':
updateBalanceDisplay(msg.payload.balances);
break;
case 'ERROR':
console.error(`[${msg.payload.errorCode}]: ${msg.payload.message}`);
break;
default:
dispatchNotification(msg);
}
}

Step 4 — Keep the Connection Alive

// In Node.js (ws library):
ws.on('ping', () => ws.pong());

// Browser WebSocket handles ping/pong automatically.

Guide 2: Fetch Fees and Submit a Payment

Prerequisites: Guide 1 (connected). See FPSF-SS-001 Guides for constructing the TransferRequest payload.

Step 1 — Fetch the Nonce

const noncePayload = {
requestId: crypto.randomUUID(),
domainSeparator: TOKEN_DOMAIN_SEPARATOR,
};
ws.send(JSON.stringify(await buildSignedMessage(signer, 'GET_NONCE', noncePayload)));
// In your message handler, case 'NONCE_RESULT': nonce = msg.payload.nonce

Step 2 — Fetch the Fee Breakdown

const feesPayload = {
requestId: crypto.randomUUID(),
domainSeparator: TOKEN_DOMAIN_SEPARATOR,
principal: principalAmount.toString(),
acquirerId: acquirerId ?? '0x00000000000000000000000000000000', // Zero-UUID if none
};
ws.send(JSON.stringify(await buildSignedMessage(signer, 'GET_FEES', feesPayload)));

// In your message handler, case 'FEES_RESULT':
// const { operatorFee, acquiringFee, totalWithFees } = msg.payload.brokenDownAmount;
// Use totalWithFees as PermitParams.value.

Step 3 — Subscribe to Transfers Before Submitting

Subscribe to the transfer channel before submitting to guarantee you receive the settlement confirmation.

const subPayload = {
requestId: crypto.randomUUID(),
domainSeparators: [TOKEN_DOMAIN_SEPARATOR],
};
ws.send(JSON.stringify(await buildSignedMessage(signer, 'SUBSCRIBE_TRANSFERS', subPayload)));

Step 4 — Construct the TransferRequest and Submit

Construct the full TransferRequest per FPSF-SS-001 Guides. Note: orderReference and acquirerId are separate bytes16 fields in PayWithPermitParams — do not concatenate them.

const submitPayload = {
requestId: crypto.randomUUID(),
transferRequest: transferRequest, // constructed per FPSF-SS-001
};
ws.send(JSON.stringify(await buildSignedMessage(signer, 'SUBMIT_PAYMENT', submitPayload)));

Step 5 — Handle Status and Settlement

function dispatchNotification(msg) {
switch (msg.type) {
case 'SUBMIT_PAYMENT_ACK':
trackPayload(msg.payload.payloadId);
break;

case 'SUBMISSION_STATUS':
if (msg.payload.status === 'FAILURE') {
showError(msg.payload.failureReason);
}
// Do NOT show success here — wait for TRANSFER_NOTIFICATION
break;

case 'TRANSFER_NOTIFICATION':
// This is final settlement.
if (msg.payload.transfer.direction === 'OUT') {
showPaymentConfirmed(msg.payload.transfer);
}
break;
}
}

Guide 3: Subscribe to Balance Updates

async function subscribeToBalances(ws, signer, domainSeparators) {
const payload = { requestId: crypto.randomUUID(), domainSeparators };
ws.send(JSON.stringify(await buildSignedMessage(signer, 'SUBSCRIBE_BALANCE', payload)));
}

// Handle push:
// case 'BALANCE_UPDATE':
// updateUI(msg.payload.domainSeparator, msg.payload.balance);

Guide 4: Reconnection Handler

async function onReconnect(ws, signer) {
// Re-register subscriptions — they do not survive connection close
await subscribeToBalances(ws, signer, myTokenList);
await subscribeToTransfers(ws, signer, myTokenList);

// Resync state — notifications during disconnection were not delivered
const balPayload = { requestId: crypto.randomUUID(), domainSeparators: myTokenList };
ws.send(JSON.stringify(await buildSignedMessage(signer, 'GET_BALANCE', balPayload)));
}

Common Mistakes

MistakeResultFix
Using totalWithFees from a previous sessionStale fees; permit may be incorrectCall GET_FEES fresh before each payment
Concatenating orderReference and acquirerIdInvalid Binding SignatureThey are separate bytes16 parameters — never concatenate
Not subscribing to transfers before submittingFinal confirmation never receivedSubscribe before SUBMIT_PAYMENT
Treating SUCCESS status as final settlementPremature confirmationWait for TRANSFER_NOTIFICATION
Not re-subscribing after reconnectionNo notifications on new connectionRe-register in reconnection handler
Deadline set in the pastEXPIRED_DEADLINE errorSet deadline at least 60 seconds in the future

FPSF-SS-002 v1.0.0 · Draft · Fabric Payment Standards Foundation · Apache-2.0