Skip to main content

Documentation Index

Fetch the complete documentation index at: https://glide-9da73dea.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Pure encoders for Glide’s social-recovery primitives on two chains. The EVM side targets Safe + Zodiac Delay Module v1.0.1: deploy and enable the module, queue a recovery action into the 72-hour cooldown, execute after cooldown, cancel via setTxNonce, and read on-chain queue state. The Solana side targets Squads v4: compose the propose-triple (config-tx-create + proposal-create + proposal-approve) for owner and threshold changes, plus proposal-cancel and config-transaction-execute for the veto and execute paths. Every export is a pure function that produces calldata bytes (EVM) or EncodedInstruction (a serialisable wire shape exporting accountMetas[], data, programId — convertible to web3.js TransactionInstruction via the helper) objects (Solana). The package is I/O-free: operators wire their own KMS-held signer, chain RPC client, and DB. No signer loading, no network calls, no state machine persistence — those live in the operator’s application layer.

Install

npm install @glideco/recovery
npmjs.com/package/@glideco/recovery

Why pure encoders?

A recovery package that bundles a KMS client, an RPC provider, and a state machine is hard to audit, hard to test offline, and harder to replace when dependencies change. Pure encoders separate the cryptographic and structural work (what bytes to sign) from the operational work (who holds the key, which RPC to call, how to persist state). The operator owns the sensitive parts; this package owns the encoding. The allowlist of accepted instruction discriminators (Solana) belongs in the encoder package rather than the broadcaster because the broadcaster is the only thing that can enforce it, and it needs the canonical set from the same source that builds the instructions.

EVM — enable recovery on a Safe

The first step generates the deterministic Delay Module address and a two-call bundle (deploy + enable). One user-signed Safe transaction activates recovery:
import {
  composeEnableRecoveryModuleCalls,
  RECOVERY_COOLDOWN_SECONDS,
  RECOVERY_EXPIRATION_SECONDS,
} from '@glideco/recovery';

const safeAddress = '0xA1B2c3D4e5F6a7B8c9D0e1F2a3B4c5D6e7F8a9B0';
const recoveryKeyAddress = '0xC3D4e5F6a7B8c9D0e1F2a3B4c5D6e7F8a9B0C1D2'; // Glide KMS (Seat R)

const setup = composeEnableRecoveryModuleCalls({
  // The Safe address — enableModule must be called by the Safe itself
  safe: safeAddress,
  // DelayInitializerInput: configures the Delay module's owner, avatar, target, cooldown
  initializer: {
    owner: recoveryKeyAddress,   // address allowed to queue recovery txs
    avatar: safeAddress,          // the Safe whose balance is acted on
    target: safeAddress,          // where execTransactionFromModule routes (== avatar)
    cooldown: RECOVERY_COOLDOWN_SECONDS,     // 72h default
    expiration: RECOVERY_EXPIRATION_SECONDS, // 7d window to execute after cooldown
  },
  // Optional: masterCopy defaults to Zodiac Delay v1.0.1
  // Optional: saltNonce defaults to 0n (first deployment on this Safe+chain pair)
});

// setup.moduleAddress — deterministic CREATE2 address for the Delay module
// setup.deploy       — { to, value, data } call to deploy via ModuleProxyFactory
// setup.enable       — { to, value, data } call to enableModule on the Safe

// Combine into a Safe multi-send and collect the user's signature:
const safeTx = buildMultiSend([setup.deploy, setup.enable]);

EVM — queue and execute a recovery action

composeRecoveryAction is the high-level entry point. It takes the Safe + Delay module addresses and a discriminated action spec, and returns the innerTx to persist plus the pre-composed queueCall and executeCall the router broadcasts:
import { composeRecoveryAction } from '@glideco/recovery';

// Recovery key proposes rotating to a new owner (e.g., after key loss):
const composed = composeRecoveryAction({
  safe: '0xA1B2c3D4e5F6a7B8c9D0e1F2a3B4c5D6e7F8a9B0',
  delayModuleAddress: setup.moduleAddress,
  action: {
    kind: 'swap_owner',
    prevOwner: '0x0000000000000000000000000000000000000001', // sentinel — caller reads Safe.getOwners()
    oldOwner:  '0xB2C3d4E5f6A7b8C9D0e1F2a3B4C5d6E7F8a9B0C1',  // lost key
    newOwner:  '0xC3D4e5F6a7B8c9D0e1F2a3B4c5D6e7F8a9B0C1D2',  // replacement key
  },
});

// Persist composed.innerTx to DB (status='pending')
// composed.queueCall  — { to: delayModule, value: '0', data: '0x...' } — recovery key signs + broadcasts
// composed.executeCall — { to: delayModule, value: '0', data: '0x...' } — anyone broadcasts after cooldown
// composed.safe, composed.delayModuleAddress — echo of inputs (lowercased)
// composed.kind    — 'swap_owner'
// composed.summary — human-readable one-liner for audit logs

// After 72h cooldown, broadcast the pre-composed execute call:
// (re-read innerTx from DB to ensure byte-stable replay)
const executeCall = composed.executeCall;

EVM — read on-chain Delay Module queue state

import { readDelayModuleQueueState } from '@glideco/recovery';
import { JsonRpcProvider } from 'ethers';

const provider = new JsonRpcProvider('https://mainnet.base.org');

const state = await readDelayModuleQueueState({
  delayModuleAddress: setup.moduleAddress,
  provider,
});
// state = {
//   cooldownSeconds: 259200,   // 72h
//   expirationSeconds: 604800, // 7d window to execute after cooldown
//   queueNonce: 1n,            // next item to queue
//   txNonce: 0n,               // next item to execute (execute < queue = items pending)
// }

Solana — Squads v4 recovery

The Squads encoder expands swap_owner into the two-instruction sequence (RemoveMember + AddMember) because Squads v4 has no atomic SwapMember instruction. The propose-triple is always three instructions: config-tx-create, proposal-create, proposal-approve:
import {
  composeSolanaRecoveryAction,
  composeSolanaCancelRecoveryInstruction,
  recomposeSolanaExecuteRecoveryInstruction,
} from '@glideco/recovery';

// Seat R (recovery key) proposes swapping a lost member:
const composed = composeSolanaRecoveryAction({
  multisigPda: 'GrAkKfEpTKQuVHG2Y97Y2FF4i7y7Q5AHLK94KB5hwkhU',
  transactionIndex: BigInt(7),
  currentThreshold: 2,
  creator: '4Nd1mBQtrMJVYVfKf2PX98HegncxXSystemRecoveryKey',  // Seat R base58 pubkey
  recoveryActionId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',   // DB row UUID — embedded in on-chain memo
  action: {
    kind: 'swap_owner',
    oldMember: '4Nd1mBQtrMJVYVfKf2PX98HegncxXLostKeyPubkey1',  // base58 — lost key
    newMember: 'DHmk7x1qGQUos2nsfQ3sJMnFHRWXYReplacementKey1', // base58 — replacement key
  },
});

// composed.proposeInstructions  — [configTxCreate, proposalCreate, proposalApprove] (EncodedInstruction[])
// composed.executeInstruction   — pre-composed configTransactionExecute (EncodedInstruction)
// composed.innerTx              — SolanaInnerTx payload to persist in DB for replay
// composed.multisigPda          — echo of input
// composed.transactionIndex     — decimal string of the u64 index
// composed.kind                 — 'swap_owner'
// composed.summary              — human-readable one-liner for audit logs

// User-initiated veto before cooldown ends:
const cancel = composeSolanaCancelRecoveryInstruction({
  multisigPda: 'GrAkKfEpTKQuVHG2Y97Y2FF4i7y7Q5AHLK94KB5hwkhU',
  transactionIndex: BigInt(7),
  member: 'UserSeatAPubkeyBase58xxxxxxxxxxxxxxxxxxxxxxxxxxxx',  // user's Seat A pubkey
  recoveryActionId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',   // same DB row UUID — for cancel memo
});
// returns a single EncodedInstruction (proposalCancel)

// Execute after cooldown — recompose from the persisted inner_tx:
const exec = recomposeSolanaExecuteRecoveryInstruction({
  innerTx: composed.innerTx,   // SolanaInnerTx from DB — NOT re-derived from scratch
  member: '4Nd1mBQtrMJVYVfKf2PX98HegncxXSystemRecoveryKey', // Seat R — any multisig member works
});
// returns a single EncodedInstruction (configTransactionExecute)

Solana — instruction-discriminator allowlist

The package exports the canonical set of accepted instruction discriminators for the broadcaster’s calldata-pinning check. Any instruction outside this set in a recovery transaction is rejected before broadcast:
import {
  RECOVERY_ALLOWED_INSTRUCTION_HEX,
  extractInstructionDiscriminatorHex,
} from '@glideco/recovery';

for (const ix of versionedTx.message.compiledInstructions) {
  const disc = extractInstructionDiscriminatorHex(ix.data);
  if (!RECOVERY_ALLOWED_INSTRUCTION_HEX.has(disc)) {
    throw new Error(`recovery tx contains a non-recovery instruction: ${disc}`);
  }
}
The allowlist covers config-tx-create, proposal-create, proposal-approve, proposal-cancel, and config-transaction-execute discriminators for Squads v4.

What this package does NOT include

  • KMS or file-based signer loading (ethers.Wallet, Keypair.fromSecretKey, etc.).
  • Database persistence of the multi-step recovery state machine.
  • The Inngest cron that ticks queued EVM recoveries through cooldown → execute.
  • Authorization checks — “is this principal allowed to start recovery on this vault?”
These live in apps/web/src/server/lib/user-multisig/ in the source repo as a reference implementation operators can fork.

Reading list

  • Money-safety contracts — F2 (CAS-claim before broadcast) applies to the recovery broadcaster that consumes this package’s output.
  • @glideco/grant-wrapper — the recovery flow issues a scoped grant for Seat R; verifyGrant gates every recovery-tool call before encoding begins.
  • Zodiac Delay Module — upstream module reference; the constants and ABI fragments in this package target v1.0.1.
  • Source on GitHub