Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
cmd committed Jan 26, 2024
1 parent 4de319b commit fe1f307
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 41 deletions.
149 changes: 113 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 : [
Expand Down Expand Up @@ -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!
Expand Down
6 changes: 3 additions & 3 deletions src/client/api/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
2 changes: 1 addition & 1 deletion test/client/contract/list_contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion test/client/contract/read_contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit fe1f307

Please sign in to comment.