Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
73 changes: 73 additions & 0 deletions bindings/node/__test__/index.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
importWalletPrivateKey,
signTransaction,
signMessage,
signHash,
signAuthorization,
createPolicy,
listPolicies,
getPolicy,
Expand Down Expand Up @@ -224,6 +226,29 @@ describe('@open-wallet-standard/core', () => {
deleteWallet('tx-signer', vaultDir);
});

it('signs raw hashes and EIP-7702 authorizations in owner mode', () => {
const privkey = '4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318';
const wallet = importWalletPrivateKey('hash-owner', privkey, undefined, vaultDir, 'evm');

const hashSig = signHash(wallet.id, 'base', '11'.repeat(32), undefined, undefined, vaultDir);
assert.ok(hashSig.signature.length > 0);
assert.ok(hashSig.recoveryId === 0 || hashSig.recoveryId === 1);

const authSig = signAuthorization(
wallet.id,
'base',
'0x1111111111111111111111111111111111111111',
'7',
undefined,
undefined,
vaultDir,
);
assert.ok(authSig.signature.length > 0);
assert.ok(authSig.recoveryId === 0 || authSig.recoveryId === 1);

deleteWallet(wallet.id, vaultDir);
});

// ---- Determinism ----

it('produces deterministic signatures', () => {
Expand Down Expand Up @@ -376,4 +401,52 @@ describe('@open-wallet-standard/core', () => {
deletePolicy('test-exe-deny', vaultDir);
deleteWallet(wallet.id, vaultDir);
});

it('signs raw hashes and authorizations through the API-key path', () => {
const wallet = createWallet('hash-policy-test', undefined, 12, vaultDir);

createPolicy(JSON.stringify({
id: 'test-hash-base-only',
name: 'Base Only Hash',
version: 1,
created_at: '2026-03-22T00:00:00Z',
rules: [
{ type: 'allowed_chains', chain_ids: ['eip155:8453'] },
],
action: 'deny',
}), vaultDir);

const key = createApiKey('hash-agent', [wallet.id], ['test-hash-base-only'], '', null, vaultDir);

const hashSig = signHash(wallet.id, 'base', '22'.repeat(32), key.token, null, vaultDir);
assert.ok(hashSig.signature.length > 0);

const authSig = signAuthorization(
wallet.id,
'base',
'0x1111111111111111111111111111111111111111',
'7',
key.token,
null,
vaultDir,
);
assert.ok(authSig.signature.length > 0);

assert.throws(
() => signAuthorization(
wallet.id,
'ethereum',
'0x1111111111111111111111111111111111111111',
'7',
key.token,
null,
vaultDir,
),
(err) => err.message.includes('not in allowlist'),
);

revokeApiKey(key.id, vaultDir);
deletePolicy('test-hash-base-only', vaultDir);
deleteWallet(wallet.id, vaultDir);
});
});
32 changes: 16 additions & 16 deletions bindings/node/package-lock.json

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

2 changes: 1 addition & 1 deletion bindings/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"build:debug": "napi build --platform",
"prepublishOnly": "napi build --platform --release",
"publish:all": "node publish.mjs",
"test": "node --test __test__/index.spec.mjs"
"test": "napi build --platform --features fast-kdf && node --test __test__/index.spec.mjs"
},
"devDependencies": {
"@napi-rs/cli": "^2.18.0"
Expand Down
52 changes: 52 additions & 0 deletions bindings/node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,58 @@ pub fn sign_typed_data(
.map_err(map_err)
}

/// Sign a raw 32-byte hash on a secp256k1-backed chain. Returns hex-encoded signature.
#[napi]
pub fn sign_hash(
wallet: String,
chain: String,
hash_hex: String,
passphrase: Option<String>,
index: Option<u32>,
vault_path_opt: Option<String>,
) -> Result<SignResult> {
ows_lib::sign_hash(
&wallet,
&chain,
&hash_hex,
passphrase.as_deref(),
index,
vault_path(vault_path_opt).as_deref(),
)
.map(|r| SignResult {
signature: r.signature,
recovery_id: r.recovery_id.map(|v| v as u32),
})
.map_err(map_err)
}

/// Sign an EIP-7702 authorization tuple. Returns hex-encoded signature.
#[napi]
pub fn sign_authorization(
wallet: String,
chain: String,
address: String,
nonce: String,
passphrase: Option<String>,
index: Option<u32>,
vault_path_opt: Option<String>,
) -> Result<SignResult> {
ows_lib::sign_authorization(
&wallet,
&chain,
&address,
&nonce,
passphrase.as_deref(),
index,
vault_path(vault_path_opt).as_deref(),
)
.map(|r| SignResult {
signature: r.signature,
recovery_id: r.recovery_id.map(|v| v as u32),
})
.map_err(map_err)
}

// ---------------------------------------------------------------------------
// Policy management
// ---------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions bindings/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ print(sig["signature"])
| `export_wallet(name_or_id, passphrase?, vault_path?)` | Export a wallet's mnemonic or keys |
| `rename_wallet(name_or_id, new_name, vault_path?)` | Rename a wallet |
| `sign_message(wallet, chain, message, passphrase?, encoding?, index?, vault_path?)` | Sign a message with chain-specific formatting |
| `sign_hash(wallet, chain, hash_hex, passphrase?, index?, vault_path?)` | Sign a raw 32-byte hash on a secp256k1-backed chain |
| `sign_authorization(wallet, chain, address, nonce, passphrase?, index?, vault_path?)` | Sign an EIP-7702 authorization tuple |
| `sign_typed_data(wallet, chain, typed_data_json, passphrase?, index?, vault_path?)` | Sign EIP-712 typed data (EVM only) |
| `sign_transaction(wallet, chain, tx_hex, passphrase?, index?, vault_path?)` | Sign a raw transaction |
| `sign_and_send(wallet, chain, tx_hex, passphrase?, index?, rpc_url?, vault_path?)` | Sign and broadcast a transaction |
Expand Down
4 changes: 4 additions & 0 deletions bindings/python/python/ows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
sign_transaction,
sign_message,
sign_typed_data,
sign_hash,
sign_authorization,
sign_and_send,
create_policy,
list_policies,
Expand All @@ -38,6 +40,8 @@
"sign_transaction",
"sign_message",
"sign_typed_data",
"sign_hash",
"sign_authorization",
"sign_and_send",
"create_policy",
"list_policies",
Expand Down
62 changes: 62 additions & 0 deletions bindings/python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,66 @@ fn sign_typed_data(
})
}

/// Sign a raw 32-byte hash on a secp256k1-backed chain.
#[pyfunction]
#[pyo3(signature = (wallet, chain, hash_hex, passphrase=None, index=None, vault_path_opt=None))]
fn sign_hash(
wallet: &str,
chain: &str,
hash_hex: &str,
passphrase: Option<&str>,
index: Option<u32>,
vault_path_opt: Option<String>,
) -> PyResult<PyObject> {
let result = ows_lib::sign_hash(
wallet,
chain,
hash_hex,
passphrase,
index,
vault_path(vault_path_opt).as_deref(),
)
.map_err(map_err)?;

Python::with_gil(|py| {
let dict = pyo3::types::PyDict::new(py);
dict.set_item("signature", &result.signature)?;
dict.set_item("recovery_id", result.recovery_id)?;
Ok(dict.unbind().into())
})
}

/// Sign an EIP-7702 authorization tuple.
#[pyfunction]
#[pyo3(signature = (wallet, chain, address, nonce, passphrase=None, index=None, vault_path_opt=None))]
fn sign_authorization(
wallet: &str,
chain: &str,
address: &str,
nonce: &str,
passphrase: Option<&str>,
index: Option<u32>,
vault_path_opt: Option<String>,
) -> PyResult<PyObject> {
let result = ows_lib::sign_authorization(
wallet,
chain,
address,
nonce,
passphrase,
index,
vault_path(vault_path_opt).as_deref(),
)
.map_err(map_err)?;

Python::with_gil(|py| {
let dict = pyo3::types::PyDict::new(py);
dict.set_item("signature", &result.signature)?;
dict.set_item("recovery_id", result.recovery_id)?;
Ok(dict.unbind().into())
})
}

/// Sign and broadcast a transaction.
#[pyfunction]
#[pyo3(signature = (wallet, chain, tx_hex, passphrase=None, index=None, rpc_url=None, vault_path_opt=None))]
Expand Down Expand Up @@ -426,6 +486,8 @@ fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sign_transaction, m)?)?;
m.add_function(wrap_pyfunction!(sign_message, m)?)?;
m.add_function(wrap_pyfunction!(sign_typed_data, m)?)?;
m.add_function(wrap_pyfunction!(sign_hash, m)?)?;
m.add_function(wrap_pyfunction!(sign_authorization, m)?)?;
m.add_function(wrap_pyfunction!(sign_and_send, m)?)?;
m.add_function(wrap_pyfunction!(create_policy, m)?)?;
m.add_function(wrap_pyfunction!(list_policies, m)?)?;
Expand Down
Loading
Loading