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
| Mistake | Result | Fix |
|---|---|---|
Using totalWithFees from a previous session | Stale fees; permit may be incorrect | Call GET_FEES fresh before each payment |
Concatenating orderReference and acquirerId | Invalid Binding Signature | They are separate bytes16 parameters — never concatenate |
| Not subscribing to transfers before submitting | Final confirmation never received | Subscribe before SUBMIT_PAYMENT |
Treating SUCCESS status as final settlement | Premature confirmation | Wait for TRANSFER_NOTIFICATION |
| Not re-subscribing after reconnection | No notifications on new connection | Re-register in reconnection handler |
| Deadline set in the past | EXPIRED_DEADLINE error | Set deadline at least 60 seconds in the future |
FPSF-SS-002 v1.0.0 · Draft · Fabric Payment Standards Foundation · Apache-2.0