← Book map

Chapter 02 · Foundations

Two functions, nine callers

buildGenericRequestFromDetails and signRequest do all the work that is not tab-specific. Everything else in the app is validation and flag-setting.

The shared request pipeline Form fields are validated, a details object is built and wrapped in a generic request, signed through the daemon, encoded as a deeplink and rendered as a QR code. form JSON validated per tab details object wrapped in an ordinal GenericRequest flags + URIs + stub sig RPC signData over the raw hash QR deeplink buildGenericRequestFromDetails() and signRequest() are the two functions every tab shares. Everything before step 2 and after step 5 is tab-specific.

FIGURE 2.1 · The six steps, with the shared ones highlighted.

The two functions that matter

src/routes/utils.ts is 251 lines and contains the entire shared protocol. If you read only two functions in this repo, read these.

buildGenericRequestFromDetails

const req = new primitives.GenericRequest({
  details,                                        // ordinal-wrapped request objects
  createdAt: new BN((Date.now() / 1000).toFixed(0)), // seconds, not ms
  responseURIs,                                   // built from the redirects field
  flags: isTestnet ? FLAG_IS_TESTNET : BASE_FLAGS,
  requestID
});

if (signed) {
  req.signature = new VerifiableSignatureData({ systemID, identityID });
  req.setSigned();
}

Three details carry weight. createdAt must be seconds — the library serialises it as a uint32 and a millisecond value silently truncates. FLAG_IS_TESTNET is passed as the initial flags value, not OR'd in later; setIsTestnet() does the OR for you. And the signature object is created here but empty — the actual bytes are filled in by signRequest.

buildResponseUris

Form valueResponseURI typeWallet behaviour
"1"TYPE_REDIRECTRedirect the user back to the URI (browser/deeplink)
"2"TYPE_POSTPOST the signed response to the URI
anything else—Silently dropped; no URI is added

The silent drop is worth knowing: a typo in the redirect type produces a request with no callback and no error, and the UI will happily show a QR that cannot answer.

signRequest

const rawHash = request.getRawDataSha256(false);        // no signature field
const sigRes  = await verusId.interface.signData({
  address: signingId,
  datahash: rawHash.toString("hex")
});
request.signature.signatureAsVch = Buffer.from(sigRes.result.signature, "base64");

The daemon signs the hash, not the bytes — the app never sees a private key for this path. The false argument to getRawDataSha256 is the "exclude signature" switch; passing true here produces a signature over a message nobody receives. If signing fails, the error message is unusually explicit about the likely cause (the signing identity is not in the wallet), and that message is worth preserving rather than replacing with a generic 500.

Validation helpers

HelperRule worth remembering
parseAddressA string ending in @ becomes TYPE_FQN; anything else must pass fromBase58Check and becomes TYPE_I_ADDRESS
requireStringTrimmed, non-empty, or ValidationError
parseNumber / parseOptionalPositiveNumberPositive and finite; the optional form floors to an integer (used for heights and expiry)
parseJsonFieldAccepts either a JSON string or an object; parse errors surface as validation errors

ValidationError maps to HTTP 400; anything else is a 500 and gets logged. That split is the reason the UI can show useful messages without leaking stack traces.

Where the chain id comes from

RPC_PORT === 18843  →  VRSCTEST
  system id  iJhCezBExJHvtyH3fGhNnt2NhU4Ztkf2yq
otherwise           →  VRSC
  system id  i5w5MuNik5NtLcYmNzcvaoixooEebB6MGV

getRpcConfig() derives isTestnet from the port, and the signing path repeats that comparison rather than passing a flag. It is consistent, but it means a malformed RPC_PORT silently lands you on mainnet semantics — check the port first when debugging.

One pipeline, nine callers. If two tabs behave differently in production, the difference is almost always in what they pass to these two functions, not in how the functions behave.

Reading the code

src/routes/utils.ts
├── parseAddress / requireString / parseNumber / parseJsonField
├── parseRedirectsField / buildResponseUris
├── buildGenericRequestFromDetails
├── signRequest
└── getRpcConfig / getServiceSignerConfig