@satoshai/playstacks
v0.3.0
Published
E2E testing SDK for Stacks blockchain dApps. Playwright + real transactions.
Downloads
412
Maintainers
Readme
@satoshai/playstacks
E2E testing SDK for Stacks blockchain dApps. Uses Playwright to drive a real browser with a mock Xverse wallet — your tests sign and broadcast real transactions without manual wallet interaction.
Install
npm install @satoshai/playstacks @playwright/testQuick start
import { testWithStacks, expect } from '@satoshai/playstacks';
const test = testWithStacks({
mnemonic: process.env.TEST_MNEMONIC!,
network: 'mainnet',
});
test('supply STX on a DeFi protocol', async ({ page, stacks }) => {
await page.goto('https://app.example.com');
// Connect wallet — the mock auto-approves
await page.getByRole('button', { name: 'Connect Wallet' }).click();
await page.getByRole('button', { name: /xverse/i }).click();
// Verify wallet is connected
const shortAddr = stacks.wallet.address.slice(0, 8);
await expect(page.getByText(shortAddr)).toBeVisible();
// Trigger a contract call — signed and broadcast automatically
await page.getByRole('button', { name: 'Supply' }).click();
// Wait for on-chain confirmation
const txid = stacks.wallet.lastTxId()!;
const result = await stacks.waitForTx(txid);
expect(result.status).toBe('success');
});Configuration
testWithStacks() accepts a config object and returns a Playwright test function with a stacks fixture.
With mnemonic (same seed phrase as your Xverse wallet)
const test = testWithStacks({
mnemonic: process.env.TEST_MNEMONIC!,
accountIndex: 0, // default: 0
network: 'mainnet',
});With private key
const test = testWithStacks({
privateKey: process.env.TEST_STX_KEY!,
network: 'testnet',
});Fee management
const test = testWithStacks({
mnemonic: process.env.TEST_MNEMONIC!,
network: 'mainnet',
fee: {
multiplier: 1.0, // multiplier on estimated fee (default: 1.0)
maxFee: 30_000, // cap in microstacks (default: 500_000 = 0.5 STX)
// fixed: 5_000, // skip estimation, use exact fee
},
});Confirmation polling
const test = testWithStacks({
mnemonic: process.env.TEST_MNEMONIC!,
network: 'mainnet',
confirmation: {
timeout: 120_000, // max wait time in ms (default: 120_000)
pollInterval: 2_000, // polling interval in ms (default: 2_000)
},
});Custom network
const test = testWithStacks({
mnemonic: process.env.TEST_MNEMONIC!,
network: { url: 'http://localhost:3999' },
});Fixture API
Every test gets a stacks fixture with:
stacks.wallet
| Property | Type | Description |
|---|---|---|
| address | string | STX address derived from your key |
| publicKey | string | Public key hex |
| rejectNext() | () => void | Flag the next wallet request to throw a user rejection error |
| lastTxId() | () => string \| null | Last broadcast transaction ID |
stacks.waitForTx(txid)
Poll until a transaction reaches a terminal status (success, abort_by_response, or abort_by_post_condition). Returns { txid, status, blockHeight }.
stacks.callReadOnly(options)
Call a read-only Clarity function (no transaction created).
import { Cl, cvToValue } from '@satoshai/playstacks';
const result = await stacks.callReadOnly({
contract: 'SP2C2YFP12AJZB1MANAT0P5GKW46QDR1G06.zest-reward-dist',
functionName: 'get-balance',
functionArgs: [Cl.principal(stacks.wallet.address)],
});
console.log(cvToValue(result));Or with split address:
await stacks.callReadOnly({
contractAddress: 'SP2C2YFP12AJZB1MANAT0P5GKW46QDR1G06',
contractName: 'zest-reward-dist',
functionName: 'get-balance',
functionArgs: [Cl.principal(stacks.wallet.address)],
});stacks.getBalance(address?)
Get STX balance in microstacks. Defaults to the wallet address.
stacks.getNonce(address?)
Get account nonce. Defaults to the wallet address.
Supported wallet methods
| Method | Description | Since |
|---|---|---|
| getAddresses / stx_getAddresses | Returns STX address + public key | v0.1 |
| wallet_connect | Xverse-compatible connect flow | v0.1 |
| stx_callContract | Sign + broadcast contract call | v0.1 |
| stx_transferStx | Sign + broadcast STX transfer | v0.1 |
| stx_signMessage | Sign plaintext message | v0.2 |
| stx_signStructuredMessage | Sign SIP-018 structured message | v0.2 |
| stx_signTransaction | Sign raw transaction hex | v0.2 |
Testing wallet rejections
test('handles user rejection', async ({ page, stacks }) => {
// ... connect wallet ...
// Flag the next request to be rejected
stacks.wallet.rejectNext();
// Trigger a transaction — the mock will reject it
await page.getByRole('button', { name: 'Supply' }).click();
// Assert the dApp handles the rejection gracefully
// (behaviour depends on the dApp — dialog may stay open, show error, etc.)
});Note: rejectNext() only applies to action methods (sign/send). Read-only methods like getAddresses are not affected, so the flag won't be consumed by background polling.
Re-exports
For convenience, @satoshai/playstacks re-exports:
- From
@stacks/transactions:Cl,ClarityValue,cvToJSON,cvToString,cvToValue,cvToHex - From
@playwright/test:expect
How it works
testWithStacks()creates a Playwright fixture that injects a mockwindow.StacksProviderandwindow.XverseProvidersviapage.addInitScript()before any page navigation- When the dApp calls wallet methods (
getAddresses,stx_callContract,stx_transferStx,stx_signMessage, etc.), the mock signs real transactions using your key - For transaction methods (
stx_callContract,stx_transferStx), the signed transaction is broadcast to the network - For signing-only methods (
stx_signMessage,stx_signStructuredMessage,stx_signTransaction), the signature or signed tx is returned without broadcasting - Compatible with
@stacks/connectv8 JSON-RPC format and Xverse 3-address format
Requirements
- Node.js >= 18
@playwright/test>= 1.40.0
License
MIT
