github near/intents wallet/v0.2.0
Wallet Contracts v0.2.0

pre-releaseone month ago

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-ed25519
  • wallet-webauthn-ed25519
  • wallet-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_at as seconds will produce invalid proofs.
  • timeout_secs remains seconds (u32), so the timestamp break is specific to created_at and 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 internal and external; old ops and out arguments 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.md still contains some old ops/out wording; the current code and tests use internal/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 external changes 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:

  • SetSignatureMode
  • AddExtension
  • RemoveExtension

Impact:

  • JSON containing { "op": "custom", ... } no longer deserializes.
  • Borsh payloads using discriminant 254 no longer deserialize.
  • Third-party wallet forks that depended on the old shared Custom variant 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_gas was renamed to FunctionCall.gas.
  • SDK method names changed in the same direction: min_gas(...) became gas(...), and exact_gas(...) became gas_exact(...).
  • SDK builder defaults changed: old FunctionCallAction::new(...) defaulted min_gas to 0, while current FunctionCall::name(...) defaults gas to 50 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 11 is 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() -> Self

It 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-auth to no-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 PayloadSignature JSON shape.
  • The domain prefix bytes remain NEAR_WALLET_CONTRACT/V1.

Migration Checklist

  1. Regenerate wallet state-init data with the current defuse-wallet-core / SDK.
  2. Re-derive wallet account IDs; do not assume old deterministic account IDs carry over.
  3. Update request JSON from ops/out to internal/external.
  4. Remove promise DAGs or redesign flows that require sequential promise dependencies.
  5. Update WalletOp and NearAction JSON to use payload.
  6. Rename function-call gas fields from min_gas to gas.
  7. Rename state-init action usage from state_init to deterministic_state_init.
  8. Use SHA3-256 over NEAR_WALLET_CONTRACT/V1 || borsh(current RequestMessage) for new signed requests.

Don't miss a new intents release

NewReleases is sending notifications on new releases.