From fe1f307ba701cc41c7e30ff8f143b4dc8abbd606 Mon Sep 17 00:00:00 2001 From: cmd Date: Fri, 26 Jan 2024 14:56:23 -0600 Subject: [PATCH] update --- README.md | 149 +++++++++++++++++++------ src/client/api/request.ts | 6 +- test/client/contract/list_contracts.ts | 2 +- test/client/contract/read_contract.ts | 2 +- 4 files changed, 118 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index dec3bec1..35984714 100644 --- a/README.md +++ b/README.md @@ -256,43 +256,71 @@ Read more info about the demo [here](demo/README.md). ### Create a Client +The `EscrowClient` is a basic client for consuming our API. It is designed to be used for any tasks which do not require an identity or signature. + ```ts import { EscrowClient } from '@scrow/core/client' const config = { // The URL to our escrow server. hostname : 'https://bitescrow-signet.vercel.app', - // The URL to an electrum-based indexer (your choice). + // The URL to an electrum-based indexer of your choice. oracle : 'https://mempool.space/signet', // The network you are using. network : 'signet' } - +// Create an EscrowClient using the above config. export const client = new EscrowClient(config) ``` +For a complete list of the `EscrowClient` API, [click here](docs/client.md). + ### Create a Signer +The `EscrowSigner` is used to represent a member of a contract, and perform signature operations on their behalf. + +It is designed to wrap a more basic `Signer` and `Wallet` API, which can be provided by an external software or hardware device for added security. + +By default, we provide a basic software implementation of the `Signer` and `Wallet`, plus a `Seed` utility for generating or importing seed material. + ```ts import { Seed, Signer, Wallet } from '@cmdcode/signer' import { EscrowSigner } from '@scrow/core/client' +// Import a seed using BIP39 seed words. const seed = Seed.import.from_words(user_words) - +// We can specify a pubkey that belongs to the escrow +// server, to verify any signed payloads from the server. +const host_pubkey = '31c82c5c86465b22adaa5e57a85593a7741eddc75f3699cc415af72c0dd13efd', +// We'll use the existing configuration for the client, +// plus include our Signer and Wallet interfaces. const signer_config = { ...config, + host_pubkey, signer : new Signer({ seed }), wallet : new Wallet(xpub) } - +// Create an EscrowSigner using the above config. export const signer = new EscrowSigner(signer_config) ``` +The `EscrowSigner` is designed to run in insecure environments. The `Signer` handles money flowing into a contract (via a 2-of-2 account), while the `Wallet` handles money flowing out (by generating addresses). + +The private key for the `Signer` can be considered disposable, as any credential generated by the signer can be recovered by the `Wallet`. + +The `Wallet` is created using an xpub (provided by the user), so the private key for the wallet is never exposed during the escrow process. + +For a complete list of the `EscrowSigner` API, [click here](docs/signer.md). + ### Build a Proposal +A proposal can be built any number of ways. We have provided some tools to make this drafting process easier, through the use of a `template` and `roles`. + ```ts import { create_policy, create_proposal } from '@scrow/core' +// We start with a basic template, and pass it through +// a helper method to ensure we have the correct format. const template = create_proposal({ title : 'Basic two-party contract with third-party arbitration.', expires : 14400, @@ -301,6 +329,9 @@ const template = create_proposal({ value : 15000, }) +// We can create a dictionary of roles for users to choose from. +// Each policy defines what information needs to be added to the +// proposal for a given role. const roles = { buyer : create_policy({ paths : [ @@ -332,114 +363,160 @@ const roles = { ``` +For more information on the `proposal` process, [click here](docs/proposal.md). + ### Roles and Endorsements +After the template and roles are defined, we can invite each `EscrowSigner` to join the proposal under a given role. This process allows the user to review the role information, before adding their credentials to the proposal. + +When all roles have been filled and the proposal is final, users can optionally provide a signature as proof of their endorsement of the terms. + ```ts +// Each member is an EscrowSigner object. const [ a_signer, b_signer, c_signer ] = signers - +// Define our template from earlier. let proposal = template +// Call each EscrowSigner to join the proposal as a given role. proposal = a_signer.proposal.join(proposal, roles.buyer) proposal = b_signer.proposal.join(proposal, roles.seller) proposal = c_signer.proposal.join(proposal, roles.agent) const signatures = signers.map(mbr => { + // Collect an endorsement from the user's signer. return mbr.proposal.endorse(proposal) }) +// Return our completed proposal and signatures. export { proposal, signatures } ``` ### Create a Contract +Once we have collected a complete proposal, it is easy to convert into a contract via our API. + ```ts +// Request to create a contract using the proposal and optional signatures. const res = await client.contract.create(proposal, signatures) - +// Check that the response is valid. if (!res.ok) throw new Error(res.error) - +// Unpack and return the contract data. export const { contract } = res.data ``` +For more information on the `contract` interface, [click here](docs/contract.md). ### Request a Deposit Account +Before making a deposit, we have to request an account from the escrow server. Each account is a time-locked 2-of-2 multi-signature address between the `funder` and the server `agent`. + ```ts +// Specify the lock-time that we wish to use. +const pubkey = a_signer.pubkey const locktime = 60 * 60 // 1 hour locktime - -const res = await signer.deposit.request_acct(locktime) - +// Fetch a new account from the server. +const res = await client.deposit.request_acct(pubkey, locktime) // Check the response is valid. if (!res.ok) throw new Error(res.error) - -// Unpack some of the terms. -export const { account } = res.data +// Unpack the account data. +const { account } = res.data +// Validate the account is issued by the escrow server. +a_signer.deposit.verify_acct(account) ``` +> For more information on the `account` interface, [click here](docs/deposit.md). + ### Deposit funds into a Contract +After verifying the account information, funders can safely make a deposit to the account address. Once the deposit transaction is visible in the mempool, we can grab the `utxo` data using an oracle. + +Deposits must first be registered before they can be locked to a contract. The API allows us to perform each action separately, or both at once. + +In the example below, we will register and commit the utxo to the contract using the `fund` API. + ```ts +// Unpack the address and agent_id from the account. const { address, agent_id } = account - +// Fetch our utxo from the address. const utxos = await client.oracle.get_address_utxos(address) - +// There should be at least one utxo present. if (utxos.length === 0) throw new Error('utxo not found') - -// Request the member to sign +// The utxo that we want should be at index 0. const { txspend } = utxos[0] +// To register a utxo, we require a pre-signed refund transaction. const return_tx = await signer.deposit.register_utxo(account, txspend) +// To lock the utxo, we need a batch of partial signatures. const covenant = await signer.deposit.commit_utxo(account, contract, txspend) - -// Fund the contract +// Fund the contract by submitting the registration and covenant together. const res = await client.deposit.fund(agent_id, return_tx, covenant) - // Check the response is valid. if (!res.ok) throw new Error('failed') - +// Unpack the response data, which should be the deposit and updated contract. export const { contract, deposit } = res.data ``` -### Checking a Contract +> For more information on the `deposit` interface, [click here](docs/deposit.md). + +### Contract Activation + +The contract will not activate until all the required funds are deposited and confirmed on the blockchain. + +You can use the `EscrowClient` to poll the contract endpoint periodically. Once the confirmed `balance` matches or exceeds the `total` value, the contract will activate automatically. ```ts +// Call the contract endpoint (via the cid). const res = await client.contract.read(cid) - +// Check that the response is valid. if (!res.ok) throw new Error(res.error) - +// Unpack the response data. const { contract } = res.data - +// View the funding status of the contract. +console.log('balance :', contract.balance) +console.log('pending :', contract.pending) +console.log('total :', contract.total) +// Check if the contract is active. if (contract.activated === null) { throw new Error('contract is not active') } ``` +Once the contract is active, members can start submitting their statements to the virtual machine (CVM). + ### Settle a Contract +Members can use their `EscrowSigner` to create a signed statement for the CVM, or endorse another member's statement. + +The default method for taking actions in the CVM is the `endorse` method, which accepts a threshold of digital signatures from members. + +In the below example, we will be using `endorse` method to create a signed statement, then collect additional signatures from other members. + ```ts +// The members we will be using to sign. const [ a_signer, b_signer ] = signers - +// The template statement we will be signing. const template = { - action : 'close', - method : 'endorse', - path : 'tails' + action : 'close', // We want to close the contract. + method : 'endorse', // Using the endorse method. + path : 'tails' // Using the provided path. } - +// Define we are working with the active contract from earlier. const contract = active_contract - +// Define an empty variable for our "witness" statement. let witness : WitnessData - -// Alice signs the initial statement. +// Alice create and signs the initial statement. witness = a_signer.witness.sign(contract, template) // Bob endoreses the statement from Alice. witness = b_signer.witness.endorse(contract, witness) - +// Submit the completed witness statement to the contract. const res = await client.contract.submit(contract.cid, witness) - // Check the response is valid. if (!res.ok) throw new Error(res.error) - +// The returned contract should be settled. export const settled_contract = res.data.contract ``` +> For more information on the `witness` interface, [click here](docs/witness.md). + ### API Demo Documentation coming soon! diff --git a/src/client/api/request.ts b/src/client/api/request.ts index ed41fad2..b9a2b596 100644 --- a/src/client/api/request.ts +++ b/src/client/api/request.ts @@ -20,14 +20,14 @@ export function request_contracts_api (signer : EscrowSigner) { } } -export function sign_request_api (client : EscrowSigner) { +export function sign_request_api (signer : EscrowSigner) { return ( url : string, - body : string = '{}', + body : string = '', method : string = 'GET' ) => { const content = method + url + body - return client._signer.gen_token(content) + return signer._signer.gen_token(content) } } diff --git a/test/client/contract/list_contracts.ts b/test/client/contract/list_contracts.ts index 4a7b3d21..75b09b78 100644 --- a/test/client/contract/list_contracts.ts +++ b/test/client/contract/list_contracts.ts @@ -6,7 +6,7 @@ const [ a_mbr ] = members const req_token = a_mbr.request.contracts() // Request an account for the member to use. -const ct_res = await client.contract.list(req_token) +const ct_res = await client.contract.list(a_mbr.pubkey, req_token) // Check the response is valid. if (!ct_res.ok) throw new Error(ct_res.error) diff --git a/test/client/contract/read_contract.ts b/test/client/contract/read_contract.ts index e194a908..4ece5d0d 100644 --- a/test/client/contract/read_contract.ts +++ b/test/client/contract/read_contract.ts @@ -4,7 +4,7 @@ import CONFIG from '../config.js' // Define a third-party client as a coordinator. const client = new EscrowClient(CONFIG.regtest.client) -const cid = '108abd1bcabf7c8cc4dcb8be824461b6d8146fbf3623f748bc1926ec818e42d1' +const cid = '798e5e4a51e60dea79690dcd3114f65fa510c539514e8f89d6a22beaed98473a' // Request an account for the member to use. const ct_res = await client.contract.read(cid)