WalletContracts ABI and Signature Verification Breaking Changes
Summary
Existing signed wallet requests are not valid against the current wallet contracts. The signature digest changed from SHA-256 to SHA3-256, the signed RequestMessage Borsh encoding changed, and the request/promise ABI changed from ops + recursive out DAG to internal + flat external promises.
The public method names are mostly unchanged (w_execute_signed, w_execute_extension, and views remain), but the schemas behind their arguments and some return/error/event values changed.
Signature Verification
Signing hash changed from SHA-256 to SHA3-256
Old wallet variants verified signatures over:
sha256("NEAR_WALLET_CONTRACT/V1" || borsh(RequestMessage))
Current wallet variants verify signatures over:
sha3_256("NEAR_WALLET_CONTRACT/V1" || borsh(RequestMessage))
Affected variants:
wallet-ed25519wallet-webauthn-ed25519wallet-webauthn-p256
The domain string is unchanged, but the hash algorithm is not. Old Ed25519 and WebAuthn proofs must be regenerated with the new digest.
Signed timestamp encoding changed
RequestMessage.created_at changed from serialized in Borsh as seconds (u32) serialized in Borsh as nanoseconds (u64).
Impact:
- The signed Borsh bytes changed even when the JSON timestamp text looks equivalent.
- Any custom signer that still serializes
created_atas seconds will produce invalid proofs. timeout_secsremains seconds (u32), so the timestamp break is specific tocreated_atand nonce state timestamps.
Request ABI
Request fields were renamed and retyped
Old request shape:
Request {
ops: Vec<WalletOp>,
out: PromiseDAG,
}Current request shape:
Request {
internal: Vec<WalletOp>,
external: Vec<NearPromise>,
}Impact:
- JSON clients must send
internalandexternal; oldopsandoutarguments no longer match the current ABI. - Borsh clients must serialize the new struct layout.
- Existing signed payloads using the old request encoding cannot verify.
contracts/wallet/README.mdstill contains some oldops/outwording; the current code and tests useinternal/external.
Promise DAG support was removed
Old out was a recursive PromiseDAG with after and then, allowing chained calls such as borrow.then(swap).then(repay).
Current external is a flat Vec<NearPromise>. Each promise is detached independently and executes concurrently.
Impact:
- Sequential promise dependencies cannot be represented in the current wallet request ABI.
- Flattening an old DAG into
externalchanges execution semantics. - Old JSON containing
{ "out": { "after": ..., "then": ... } }is no longer valid.
Wallet Operation ABI
WalletOp JSON encoding now wraps variant payloads
Old JSON used internally tagged enum fields:
{ "op": "add_extension", "account_id": "extension.near" }Current JSON uses payload:
{ "op": "add_extension", "payload": { "account_id": "extension.near" } }This affects set_signature_mode, add_extension, and remove_extension.
Borsh discriminants for those three operations are unchanged (0, 1, 2), but the containing Request encoding changed, so old signed requests still do not verify.
WalletOp::Custom was removed
Old ABI exposed a custom operation variant with Borsh discriminant 254, even though the base wallet contract rejected it at execution time.
Current ABI only supports:
SetSignatureModeAddExtensionRemoveExtension
Impact:
- JSON containing
{ "op": "custom", ... }no longer deserializes. - Borsh payloads using discriminant
254no longer deserialize. - Third-party wallet forks that depended on the old shared
Customvariant must define their own extension point outside the current base wallet ABI.
Promise and Action ABI
Action JSON encoding now wraps variant payloads
Old JSON:
{
"action": "function_call",
"function_name": "ft_transfer",
"args": "base64...",
"deposit": "1",
"min_gas": "30000000000000"
}Current JSON:
{
"action": "function_call",
"payload": {
"function_name": "ft_transfer",
"args": "base64...",
"deposit": "1",
"gas": "30000000000000"
}
}Impact:
- Old action JSON is not accepted by the current ABI.
FunctionCallAction.min_gaswas renamed toFunctionCall.gas.- SDK method names changed in the same direction:
min_gas(...)becamegas(...), andexact_gas(...)becamegas_exact(...). - SDK builder defaults changed: old
FunctionCallAction::new(...)defaultedmin_gasto0, while currentFunctionCall::name(...)defaultsgasto50 Tgas.
StateInit action was renamed
Old action variant:
state_init
Current action variant:
deterministic_state_init
Current JSON also nests the state init under payload.state_init; the old StateInitAction flattened it.
Impact:
- Old JSON action tags and shape do not deserialize.
- Borsh discriminant
11is still used for the state-init action, but old signed request bytes still break because the enclosing request layout changed.
State Init and Deterministic Account IDs
Stored nonce timestamp encoding changed
Nonces.last_cleaned_at changed from Borsh seconds (u32) to Borsh nanoseconds (u64).
Impact:
- State-init bytes changed.
- Deterministic account IDs derived from the same code id, public key, subwallet id, timeout, and extensions changed.
- Existing old wallet state is not Borsh-compatible with the current state layout. Do not point existing wallet accounts at the current code without an explicit migration plan.
View, Event, and Error Surface
SignedRequest event hash changed
The event name/version remains, but SignedRequest.hash is now the canonical SHA3/domain request hash described above.
Impact:
- Event consumers must treat old and current request hashes as different namespaces.
Wallet operation events are emitted after validation
Old wallet operation handlers emitted SignatureModeSet, ExtensionAdded, and ExtensionRemoved before checking whether the operation would fail. Current handlers emit those events only after the state change succeeds.
Impact:
- Failed duplicate/no-op wallet operations no longer produce these pre-check debugging events.
No-Sign Variant
The no-sign variant now exposes a payable private initializer:
w_init() -> SelfIt initializes a no-sign wallet on the current account, disables signature authentication, and adds the current account as the only extension.
Related visible changes:
- Build metadata variant name changed from
no-authtono-sign.
This is mostly an additive ABI change, but deployment scripts referencing the old no-auth variant name must be updated.
What Did Not Change
w_execute_signed(msg, proof)method name is unchanged.w_execute_extension(request)method name is unchanged.- The proof argument is still a string.
- Ed25519 public key and signature string formats remain base58 typed curve strings.
- WebAuthn proofs are still parsed as the same
PayloadSignatureJSON shape. - The domain prefix bytes remain
NEAR_WALLET_CONTRACT/V1.
Migration Checklist
- Regenerate wallet state-init data with the current
defuse-wallet-core/ SDK. - Re-derive wallet account IDs; do not assume old deterministic account IDs carry over.
- Update request JSON from
ops/outtointernal/external. - Remove promise DAGs or redesign flows that require sequential promise dependencies.
- Update
WalletOpandNearActionJSON to usepayload. - Rename function-call gas fields from
min_gastogas. - Rename state-init action usage from
state_inittodeterministic_state_init. - Use SHA3-256 over
NEAR_WALLET_CONTRACT/V1 || borsh(current RequestMessage)for new signed requests.