[FEATURE] Add stacks chain support#115
[FEATURE] Add stacks chain support#115tony1908 wants to merge 12 commits intoopen-wallet-standard:mainfrom
Conversation
|
@tony1908 is attempting to deploy a commit to the MoonPay Team on Vercel. A member of the Team first needs to authorize it. |
njdawn
left a comment
There was a problem hiding this comment.
thanks! could you verify these changes? testing locally resulted in failures.
I validated this against the repo and reproduced a concrete interoperability failure.
In the checked-out PR branch, I generated a signature with ows/crates/ows-signer/src/chains/stacks.rs and verified it with standard Stacks tooling (@stacks/encryption / verifyMessageSignatureRsv). The result was verified=false for a simple sign_message("hello stacks") round-trip.
The code path hashes the message with double SHA-256 before signing, which does not match standard Stacks message-signing verification expectations.
njdawn
left a comment
There was a problem hiding this comment.
I tested this branch locally and the happy-path Stacks tests pass, but I found a concrete correctness issue that makes the transaction-signing support too broad as currently implemented.
I generated a real unsigned 1-of-2 multisig STX transfer with @stacks/transactions, then signed the same unsigned tx two ways:
- With the canonical Stacks JS library (
makeSTXTokenTransfer(... signerKeys: [priv1])) - With this PR's actual CLI path (
cargo run -p ows-cli -- sign tx --chain stacks --tx <unsigned_multisig_hex>)
The signatures do not match.
- Official Stacks JS signature:
00a3cdc4cfef25ee355bb0d24d31259e02020079832c859d2d6bb866443babd5d63f5a22daa9852582794311a55a89ec4e141529b9f2ba21a84c16926a92ce051a - OWS PR #115 signature:
017b7ac977ac2e892e7c7143b73d66ff01e6f01e8321c8c7558ea9892314ecbf173d6afffea5237971bfe5302b63f17eb60f3bd4d0ff34754f08e43210248fa54e
The root cause appears to be the fixed single-sig layout assumptions in stacks.rs:
STACKS_SIG_OFFSETis hardcoded to44sign_transaction()clears bytes44..109encode_signed_transaction()injects the signature at bytes44..109
That works for standard single-sig P2PKH only, but for unsigned multisig the auth section after byte 44 is dynamic. In the concrete tx I generated, the payload starts at byte 50, so clearing 44..109 zeros part of the payload and produces the wrong presign hash.
So I think this PR is mergeable only if one of these happens:
- It explicitly validates and rejects anything except the supported single-sig transaction shape, or
- It implements the real Stacks auth parsing/signing path for multisig/sponsored layouts instead of assuming a fixed offset.
Validate auth_type (0x04) and hash_mode (0x00) before signing or encoding Stacks transactions. The fixed byte offsets used for presign hashing and signature injection are only valid for standard P2PKH single-sig; multisig and sponsored layouts have different auth structures and would produce wrong signatures silently. Addresses review feedback on PR open-wallet-standard#115.
|
@tony1908 how should one handle the |
njdawn
left a comment
There was a problem hiding this comment.
High — ows/crates/ows-core/src/config.rs: default_rpc() adds a Stacks endpoint, but test_load_or_default_nonexistent still expects the old RPC count. The branch currently fails this existing test and should not merge until it is updated.
|
The
This is critical because compressed and uncompressed keys derive different Stacks addresses. If the wallet derived the address from an uncompressed key, you must pass the uncompressed pubkey — otherwise the transaction will be rejected with // Compressed (33 bytes) → derives a different SP address
publicKey: '03ea909707c97c498a...'
// Uncompressed (65 bytes) → derives the correct SP address
publicKey: '04ea909707c97c498a...5fff641c3d704895cd...'In OWS, the default Stacks key derivation uses uncompressed public keys (32-byte private key without the |
my bad @njdawn missed that test, it is ready now |
|
@tony1908 makes sense, just wanted to confirm :) so in essence, the agent will have to take an extra step to derive the public key before transaction construction. i dont think there is a CLI command to extract the public key, right? so deriving it from a message signature might be the only way? |
|
Correct — there's no CLI command today to extract the public key directly. The current workarounds are:
Neither is great for an agent workflow. A |
njdawn
left a comment
There was a problem hiding this comment.
after merge conflicts fixed, ready to merge
|
@njdawn conflicts fixed, seems like vercel run need auth |
|
@njdawn there was a conflict with the new chains but it is fixed now, all tests running correctly |
What
Add Stacks (STX) as a supported chain family: wallet creation, address derivation (c32check), transaction signing (SHA-512/256 presign hash, VRS signatures), message signing (Stacks prefix), and transaction broadcast via Hiro API.
Why
Stacks is a Bitcoin L2 with growing adoption for smart contracts and DeFi. Adding native support enables OWS wallets to derive Stacks addresses from the same mnemonic as all other chains, and supports x402 payment signing workflows on Stacks.
Testing
https://explorer.hiro.so/txid/0x7ace2aac8228780e26c98dd9b8b0952b9f6bc476936ac6f442e6367f7201962d
Manual testing
Build
cargo build --release --bin ows
Create a wallet (derives addresses for all chains including Stacks)
./target/release/ows wallet create --name my-wallet
→ stacks:1 → SP...
Sign a message
./target/release/ows sign message --wallet my-wallet --chain stacks --message "hello stacks"
→ 65-byte VRS signature hex
Sign a transaction (requires an unsigned tx hex built externally)
Use @stacks/transactions to build the unsigned tx:
import { makeUnsignedSTXTokenTransfer, AnchorMode } from '@stacks/transactions';
import { StacksMainnet } from '@stacks/network';
const tx = await makeUnsignedSTXTokenTransfer({
recipient: 'SP...',
amount: BigInt(10),
publicKey: '',
network: new StacksMainnet(),
memo: 'test',
anchorMode: AnchorMode.Any,
});
console.log(Buffer.from(tx.serialize()).toString('hex'));
Then sign and broadcast with OWS:
./target/release/ows sign send-tx --wallet my-wallet --chain stacks --tx <unsigned_tx_hex>
→ broadcasts to https://api.hiro.so/v2/transactions, returns txid
Notes
pubkey — matches @stacks/transactions behavior
|| nonce) — matches @stacks/transactions exactly