class WalletClient {
constructor(port, name = `wallet:${port}`) {
this.base = `http://localhost:${port}`;
this.name = name;
this.origin = 'http://e2e-test';
}
async post(endpoint, body = {}) {
const resp = await fetch(`${this.base}/${endpoint}`, {
method: 'POST',
headers: { 'Origin': this.origin, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: AbortSignal.timeout(180_000), });
const text = await resp.text();
let data = {};
try { data = text ? JSON.parse(text) : {}; } catch { data = { _raw: text }; }
if (!resp.ok) {
const msg = data.message || data.error || data._raw || text || `HTTP ${resp.status}`;
throw new Error(`${this.name} /${endpoint} ${resp.status}: ${msg}`);
}
return data;
}
async get(endpoint) {
const resp = await fetch(`${this.base}/${endpoint}`, {
headers: { 'Origin': this.origin },
});
const text = await resp.text();
try { return text ? JSON.parse(text) : {}; } catch { return { _raw: text }; }
}
async isAuthenticated() { return this.get('isAuthenticated'); }
async getHeight() { return this.get('getHeight'); }
async getNetwork() { return this.get('getNetwork'); }
async getVersion() { return this.get('getVersion'); }
async getPublicKey(protocolId, keyId, counterparty = 'self', forSelf = false) {
return this.post('getPublicKey', { protocolId, keyId, counterparty, forSelf });
}
async identityKey() {
const r = await this.post('getPublicKey', { identityKey: true });
return r.publicKey;
}
async createSignature(data, protocolId, keyId, counterparty = 'self') {
return this.post('createSignature', { data: Array.from(data), protocolId, keyId, counterparty });
}
async verifySignature(data, signature, protocolId, keyId, counterparty = 'self', forSelf = false) {
return this.post('verifySignature', {
data: Array.from(data), signature: Array.from(signature),
protocolId, keyId, counterparty, forSelf,
});
}
async encrypt(plaintext, protocolId, keyId, counterparty = 'self') {
const data = typeof plaintext === 'string' ? Array.from(Buffer.from(plaintext)) : Array.from(plaintext);
return this.post('encrypt', { plaintext: data, protocolId, keyId, counterparty });
}
async decrypt(ciphertext, protocolId, keyId, counterparty = 'self') {
return this.post('decrypt', { ciphertext: Array.from(ciphertext), protocolId, keyId, counterparty });
}
async createAction(outputs, description, opts = {}) {
return this.post('createAction', {
outputs,
description,
options: {
signAndProcess: opts.signAndProcess ?? true,
acceptDelayedBroadcast: opts.acceptDelayedBroadcast ?? false,
randomizeOutputs: opts.randomizeOutputs ?? false,
noSend: opts.noSend ?? false,
},
labels: opts.labels,
});
}
async signAction(reference, spends = {}) {
return this.post('signAction', { reference, spends });
}
async abortAction(reference) {
return this.post('abortAction', { reference });
}
async internalizeAction(tx, outputs, description) {
return this.post('internalizeAction', { tx, outputs, description });
}
async listOutputs(basket = 'default', opts = {}) {
return this.post('listOutputs', { basket, ...opts });
}
async listActions(opts = {}) {
return this.post('listActions', { labels: [], ...opts });
}
async relinquishOutput(txid, vout, basket) {
return this.post('relinquishOutput', { txid, vout, basket });
}
async acquireCertificate(certificateType, certifier, fields, opts = {}) {
return this.post('acquireCertificate', {
certificateType,
certifier,
acquisitionProtocol: 'direct',
fields,
serialNumber: opts.serialNumber || 'AQID',
revocationOutpoint: opts.revocationOutpoint || 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef.0',
signature: opts.signature || 'deadbeef',
keyringRevealer: opts.keyringRevealer || 'certifier',
keyringForSubject: opts.keyringForSubject || opts.masterKeyring || {},
});
}
async listCertificates(certifiers = [], types = []) {
return this.post('listCertificates', { certifiers, types });
}
async proveCertificate(certificate, verifier, fieldsToReveal) {
return this.post('proveCertificate', { certificate, fieldsToReveal, verifier });
}
async relinquishCertificate(certificateType, serialNumber, certifier) {
return this.post('relinquishCertificate', { certificateType, serialNumber, certifier });
}
async discoverByIdentityKey(identityKey, limit = 10, offset = 0) {
return this.post('discoverByIdentityKey', { identityKey, limit, offset });
}
async discoverByAttributes(attributes, limit = 10, offset = 0) {
return this.post('discoverByAttributes', { attributes, limit, offset });
}
async balance() {
const r = await this.listOutputs('default', { limit: 10000, includeEnvelope: false });
const outputs = r.outputs || [];
if (Array.isArray(outputs)) {
return outputs
.filter(o => o.spendable !== false)
.reduce((sum, o) => sum + (o.satoshis || 0), 0);
}
return 0;
}
async sendTo(lockingScript, satoshis, description = 'e2e transfer') {
return this.createAction([{
lockingScript,
satoshis,
outputDescription: description,
tags: ['e2e-test'],
}], description);
}
async transferTo(recipientWallet, satoshis, description = 'e2e transfer') {
const bKey = await recipientWallet.getPublicKey(
[2, '3241645161d8'],
'SfKxPIJNgdI= NaGLC6fMH50=',
ANYONE_KEY, true,
);
const lockingScript = buildP2PKH(bKey.publicKey);
const result = await this.createAction([{
lockingScript,
satoshis,
outputDescription: description,
tags: ['e2e-test'],
}], description);
const vout = findVoutByScript(result.tx, lockingScript, satoshis);
await recipientWallet.internalizeAction(
result.tx, [{
outputIndex: vout,
protocol: 'wallet payment',
paymentRemittance: {
derivationPrefix: 'SfKxPIJNgdI=',
derivationSuffix: 'NaGLC6fMH50=',
senderIdentityKey: ANYONE_KEY,
},
}],
`Received ${satoshis} sats`,
);
return { txid: result.txid, satsSent: satoshis };
}
}
const ANYONE_KEY = '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798';
function buildP2PKH(pubkeyHex) {
const pubkey = Buffer.from(pubkeyHex, 'hex');
const { createHash } = require('crypto');
const sha = createHash('sha256').update(pubkey).digest();
const hash160 = createHash('ripemd160').update(sha).digest();
return '76a914' + hash160.toString('hex') + '88ac';
}
function findVoutByScript(txBytes, expectedScript, expectedSats) {
return 0;
}
module.exports = { WalletClient, ANYONE_KEY, buildP2PKH, findVoutByScript };