Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bd07f02
chore(build): update bundled Ant to 0.5.36 (#147)
meinharrd Jul 7, 2026
73ee4a0
feat(find): in-page search bar (Cmd/Ctrl+F)
flotob Jul 7, 2026
de47fdc
revert: feat(find): in-page search bar (Cmd/Ctrl+F)
flotob Jul 7, 2026
34cc89e
feat(wallet): SafeExecutor core for Safe smart accounts
flotob Jul 9, 2026
50c7655
feat(wallet): Safe account model, creation wizard, and activation states
flotob Jul 10, 2026
3310094
fix(wallet): breathing room between action buttons and safe status card
flotob Jul 10, 2026
8aa9814
feat(wallet): Safe sends with signature checklist and pending persist…
flotob Jul 10, 2026
5e49958
fix(wallet): resolve safe owner indexes to addresses in the send path
flotob Jul 10, 2026
6546a76
test(e2e): Safe lifecycle through the real UI against a Gnosis fork
flotob Jul 10, 2026
b15e89d
feat(wallet): asynchronous Safe signing board
flotob Jul 10, 2026
ba6aac9
feat(wallet): unfinished-transactions row with pending Safe overview
flotob Jul 10, 2026
b871f5b
fix(wallet): fail over between public signaling relays for phone signing
flotob Jul 10, 2026
6510ccf
fix(wallet): walk through vault unlock instead of erroring in Safe flows
flotob Jul 10, 2026
45f729d
feat(wallet): EIP-1271 SafeMessage signing sessions
flotob Jul 10, 2026
ade4ba1
feat(wallet): Safe accounts as dApp wallets (connect, sign, send)
flotob Jul 10, 2026
d8db024
test(wallet): stop remote-session unit tests probing live brokers
flotob Jul 10, 2026
04d8e54
Update colibri-stateless to version 2.0.0
simon-jentzsch Jul 10, 2026
9189305
fix(build): synchronize colibri lockfile
flotob Jul 12, 2026
cd17b3d
Merge pull request #161 from simon-jentzsch/patch-1
flotob Jul 12, 2026
24ef088
fix(wallet): bind SafeMessage sessions to their requesting page
flotob Jul 12, 2026
ec746ff
fix(wallet): discard a Safe's pending state when the Safe is deleted
flotob Jul 12, 2026
f9c9010
chore: merge feature/openlv into feature/safe-accounts
flotob Jul 12, 2026
ba359e4
fix(dapp): bind provider responses to the requesting document
flotob Jul 12, 2026
1c7bb95
fix(wallet): withdraw the Safe signing board when its document navigates
flotob Jul 12, 2026
05f82a3
fix(wallet): fence stale board continuations behind a session epoch
flotob Jul 12, 2026
8ebd3f0
chore(build): update bundled Ant to v0.5.41
flotob Jul 12, 2026
4d1728b
Revert "chore(build): update bundled Ant to v0.5.41"
flotob Jul 12, 2026
bc831ce
chore(build): update bundled Ant to v0.5.41
flotob Jul 12, 2026
076e0d8
Merge pull request #162 from solardev-xyz/codex/update-ant-v0.5.41-pr
flotob Jul 12, 2026
6ec033f
Merge main into feature/safe-accounts
flotob Jul 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@
"@metamask/browser-passworder": "^6.0.0",
"@openlv/core": "^0.0.2",
"@openlv/session": "^0.0.3",
"@safe-global/protocol-kit": "^8.0.3",
"@scure/bip39": "^2.2.0",
"@x402/core": "^2.12.0",
"@x402/evm": "^2.12.0",
Expand Down
121 changes: 118 additions & 3 deletions src/main/identity-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -794,14 +794,31 @@ const WALLET_TYPES = {
MNEMONIC: 'mnemonic',
LEDGER: 'ledger',
REMOTE: 'remote', // phone / other device signing over openlv
SAFE: 'safe', // Safe smart account owned by other wallet records
};

/** User-facing labels for device account types (auto-names, error text). */
/** User-facing labels for non-mnemonic account types (auto-names, error text). */
const DEVICE_LABELS = {
[WALLET_TYPES.LEDGER]: 'Ledger',
[WALLET_TYPES.REMOTE]: 'Phone',
[WALLET_TYPES.SAFE]: 'Safe',
};

/** Type-specific record fields to expose through the record seams. */
function extraRecordFields(record) {
const fields = {};
if (record.path) {
fields.path = record.path;
}
if (record.type === WALLET_TYPES.SAFE) {
fields.owners = record.owners;
fields.threshold = record.threshold;
fields.saltNonce = record.saltNonce;
fields.deployed = record.deployed || {};
}
return fields;
}

/**
* The wallet list stored in vault-meta, with the implicit pre-multi-wallet
* default (just the main wallet) when `derivedWallets` was never written.
Expand Down Expand Up @@ -846,7 +863,7 @@ function getWalletRecord(walletIndex, meta = getVaultMeta()) {
name: record.name,
address,
type: record.type || WALLET_TYPES.MNEMONIC,
...(record.path ? { path: record.path } : {}),
...extraRecordFields(record),
};
}

Expand Down Expand Up @@ -913,7 +930,7 @@ async function getDerivedWallets() {
name: wallet.name,
address,
type,
...(wallet.path ? { path: wallet.path } : {}),
...extraRecordFields(wallet),
});
}

Expand Down Expand Up @@ -999,6 +1016,79 @@ async function addRemoteWallet(name, address) {
return addDeviceWallet(WALLET_TYPES.REMOTE, name, address);
}

/**
* Add a Safe smart-account record.
*
* The init params (owners, threshold, saltNonce) are FROZEN once stored —
* they are what makes the CREATE2 address reproducible on other chains
* (retroactive deployment recovers funds sent there), so nothing may ever
* rewrite them. `owners` are wallet indexes of existing records; the
* caller (safe-service) resolves their addresses and predicts `address`
* before storing.
*
* Only the shipped presets are accepted: 1-of-2 and 2-of-3. 2-of-2 is
* deliberately not offered — losing either device bricks the funds.
*
* @param {string} name - Display name ('' → auto "Safe N")
* @param {Object} params
* @param {string} params.address - Predicted counterfactual address
* @param {number[]} params.owners - Wallet indexes of the owner records
* @param {number} params.threshold
* @param {string} params.saltNonce
* @returns {Promise<Object>} The stored record
*/
async function addSafeWallet(name, { address, owners, threshold, saltNonce }) {
const validPreset =
Array.isArray(owners) &&
((owners.length === 2 && threshold === 1) || (owners.length === 3 && threshold === 2));
if (!validPreset) {
throw new Error('A Safe needs 1 of 2 or 2 of 3 owners');
}
if (new Set(owners).size !== owners.length) {
throw new Error('Duplicate owner accounts');
}
for (const ownerIndex of owners) {
const record = getWalletRecord(ownerIndex);
if (!record) {
throw new Error(`Owner wallet index ${ownerIndex} does not exist`);
}
if (record.type === WALLET_TYPES.SAFE) {
throw new Error('A Safe cannot own another Safe');
}
}
if (typeof saltNonce !== 'string' || !/^\d+$/.test(saltNonce)) {
throw new Error('Invalid Safe salt nonce');
}

return addDeviceWallet(WALLET_TYPES.SAFE, name, address, {
owners: [...owners],
threshold,
saltNonce,
deployed: {},
});
}

/**
* Record that a Safe's contract is now live on a chain. Deployment state
* is the ONLY mutable part of a safe record — init params stay frozen.
*
* @param {number} index - Wallet index of the safe record
* @param {number} chainId
*/
async function markSafeDeployed(index, chainId) {
const meta = getVaultMeta();
if (!meta) {
throw new Error('No vault found');
}
const wallets = getWalletList(meta);
const record = wallets.find((w) => w.index === index);
if (!record || record.type !== WALLET_TYPES.SAFE) {
throw new Error(`Wallet ${index} is not a Safe account`);
}
record.deployed = { ...(record.deployed || {}), [chainId]: true };
saveVaultMeta({ ...meta, derivedWallets: wallets });
}

/**
* Get the active wallet index
* @returns {number}
Expand Down Expand Up @@ -1149,6 +1239,29 @@ async function deleteDerivedWallet(index) {
throw err;
}

// Safe owners are referenced by index; deleting one would leave the
// Safe unable to collect that signature (and break executor selection).
const owningSafe = wallets.find(
(w) => w.type === WALLET_TYPES.SAFE && (w.owners || []).includes(index)
);
if (owningSafe) {
throw new Error(
`This account is an owner of "${owningSafe.name}" — delete that Safe account first`
);
}

// A Safe's half-signed state is keyed by wallet index (safe-pending.json
// entry, in-memory SafeMessage session). Discard both WITH the record:
// a later account that reuses the index must neither inherit nor be
// blocked by the deleted Safe's leftovers. Cleanup precedes the meta
// write so a failure never leaves a deleted record with live state.
// (Lazy requires — both modules are dependency-light — keep the Safe
// stack out of ordinary wallet operations.)
if (wallets[walletIndex].type === WALLET_TYPES.SAFE) {
require('./wallet/safe/message-sessions').discardSession(index);
require('./wallet/safe/pending-store').clearPending(index);
}

// Remove from list
wallets.splice(walletIndex, 1);

Expand Down Expand Up @@ -1521,6 +1634,8 @@ module.exports = {
createDerivedWallet,
addLedgerWallet,
addRemoteWallet,
addSafeWallet,
markSafeDeployed,
renameDerivedWallet,
deleteDerivedWallet,
getActiveWalletAddress,
Expand Down
Loading
Loading