Lay2 - pw-sdk - Build DApps on CKB and Run Them Everywhere

UPDATE: Checkpoint 4 Completed

First let’s recall the goals of Checkpoint 4

pw-lock

Full version of pw-lock includes:

Support for Ethereum

Support for EOS

Support for Tron

Support for Webauthn

Support for ACP (bonus)


pw-core

Full version of pw-core includes:

Providers

EthProvider

With this provider, you can access Ethereum account in any standard Ethereum environment (where window.ethereum or window.web3 exists). EthProvider provides callback on address change, and an ensResolver method to convert ENS name to Ethereum address. Also it has a built-in sign function which provides the personalSign method from EIP-1193.

Web3ModalProvider (bonus)

With this provider, you can access Ethereum account in a non-Ethereum environment with the help of plugins, such as WallectConnect and Torus.

WalletConnect is an open source protocol for connecting decentralised applications to mobile wallets with QR code scanning or deep linking. A user can interact securely with any Dapp from their mobile phone, making WalletConnect wallets a safer choice compared to desktop or browser extension wallets.

Torus is a user-friendly key management system for DApps. Users can use their familiar SNS accounts to login and create a crypto account, providing a seamless crypto experience for Internet users.

EosProvider

With this provider, you can access ordinary EOS account (12 characters) with the help of ScatterJS and ScatterEOS. As we know that EOS account is a user defined string, and what we need is the public key behind it, so we provide a method getEosPublicKey to fetch the public key from an EOS account. And if you are going to use EOS account to create an Address instance, there is another higher level method getCKBLockArgsForEosAccount in this provider, which can directly transform an EOS account to the param needed. Also it has a built-in sign function which provides the getArbitrarySignature function from scatter-js’s schema (eosjs2).

TronProvider

With this provider, you can access Tron account in any standard TronWeb environment (where window.tronWeb exists). Also it has a built-in sign function which provides the trx.sign function from TronWeb’s API schema.

Builders

SimpleBuilder

SimpleBuilder is designed to meet the most basic needs: send some CKB to specific address. Usage of SimpleBuilder is simple:

const builder = new SimpleBuilder(
	new Address('some-eth-address', AddressType.eth),
  new Amount('100')
);

const rawTx = await builder.build();

However, simplicity does not mean it is not powerful. The first param is an Address instance, which means you can use all address types supported by pw-core, such as ETH / EOS / Tron, and of course CKB (both standard address and long address). The second param is an Amount instance, and it can accept any decimals as the second param (8 by default). Furthermore, if the address is ACP capable, the minimal value of the amount param can be adjusted automatically.

ACPBuilder (bonus)

As the name indicates, it’s a builder for sending CKB to ACP (Any Can Pay) address. First the builder will check if the address is a registered ACP address (just use address.isAcp() method), and then use the collector to fetch a live cell of this address. If the two steps succeed, the builder will return a valid ACP transaction, else it will throw corresponding exceptions.

SUDTBuilder (bonus)

This is a high level builder for assembling transactions compatible with the sUDT (Simple UDT) specification. SUDTBuilder makes sending sUDT as easy as sending CKB with SimpleBuilder, and only two params are added to specify which sudt will be sent and to determine if the sender is willing to provide CKB along with sUDT if necessary. Still, it’s not so simple as it looks like. If you know more about how CKB and sUDT works, you’ll understand that there are many different cases in assembling sUDT transactions. Let’s list all the cases we’ve covered here:

  • Sender’s sUDT cell has extra CKB for transaction fee

    • Receiver has an ACP cell to receive sUDT

      This is the simplest case of sUDT transaction. All we have to do is just put the sender’s cell as well as the receiver’s sUDT ACP cell as the inputs, and modify the sUDT amount of both cells to the correct value in the outputs. Then we calculate the transaction fee and cover it with sender’s cell capacity.

    • Receiver doen’s have an ACP cell to receive sUDT

      This case is quite common especially in the early days of sUDT adoption. First you should as k the sender’s opinion about sending extra CKB along with the sUDT if needed, and then fill the withCKB param value accordingly. Then the builder will determine which strategy to choose when assembling the transaction.

  • Sender’s sUDT cell has no extra CKB for transaction fee

    In this case, the builder will query extra available live cell of the sender’s address for transaction fee, and other processes are the same with above.

Signers

DefaultSigner

In the latest version, we combine 3 provider-related signers (EthSigner, TronSigner, EosSigner) into one - the DefaultSigner. Instead of creating a independent signer for each built-in provider, we add a sign method in Provider and require the provider instance in the constructor of DefaultSigner. This new design make it easy for developers who only need default signature from current provider.

Signer

This is the base class of all signers, including DefaultSigner. It provides the standard signing process of a regular CKB transaction. If you want to customize this process, let’s say you want to use a new hash algorigthm defined in SomeHasher, you can create a SomeSigner extends Signer and pass a SomeHasher instance in SomeSigner’s constructor, just like this:

class SomeSigner extends Signer {
  constructor() {
    super(new SomeHasher());
  }
}

Models

In order to hide the underlying details of CKB transaction and make life a lot easier for dApp developers, we provide many model classes with friendly methods. Here are some code snippets for several representative classes, and for more of them you can refer to our doc site.

Address

/* create an Address instance from a CKB address */
const ckbAddress = new Address('ckb1qyqdmeuqrsrnm7e5vnrmruzmsp4m9wacf6vsxasryq', AddressType.ckb);

/* create an Address instance from an Ethereum address */
const ethAddress = new Address('0x308f27c8595b2ee9e6a5faa875b4c1f9de6b679a', AddressType.eth);

/* get the original address string */
console.log('ckb: ', ckbAddress.addressString)
console.log('eth: ', ethAddress.addressString)
// ckb: ckb1qyqdmeuqrsrnm7e5vnrmruzmsp4m9wacf6vsxasryq
// eth: 0x308f27c8595b2ee9e6a5faa875b4c1f9de6b679a

/* get the corresponding CKB address */
console.log('ckb: ', ckbAddress.toCKBAddress())
console.log('eth: ', ethAddress.toCKBAddress())
// ckb: ckb1qyqdmeuqrsrnm7e5vnrmruzmsp4m9wacf6vsxasryq
// eth: ckt1q3vvtay34wndv9nckl8hah6fzzcltcqwcrx79apwp2a5lkd07fdxxvy0yly9jkewa8n2t74gwk6vr7w7ddne5jrkf6c

/* get the corresponding lock script hash (with the toHash method of class Script) */
console.log('ckb: ', ethAddress.toLockScript().toHash())
console.log('eth: ', ethAddress.toLockScript().toHash())
// ckb: 0xe9e412caf497c69e9612d305be13f9173752b9e75bc5a9b6d1ca51eb38d07d59
// eth: 0x0963476f28975bf93da673cd2442bd69c4b2d4e720af5a67ecece8a03b8926b5

/* check if the address is an ACP address */
console.log('ckb: ', ckbAddress.isAcp())
console.log('eth: ', ethAddress.isAcp())
// false
// true

// get the minimal CKB amount (an Amount instance) you can transfer to this address
console.log('ckb: ', ckbAddress.minPaymentAmount().toString() + ' CKB')
console.log('eth: ', ethAddress.minPaymentAmount().toString() + ' CKB')
// 61 CKB
// 0.00000001 CKB

Amount

const ckb100 = new Amount('100');
const shannon100 = new Amount('100', AmountUnit.shannon);
const usdt = new Amount('1234.5678', 6); // Assume usdt's decimals is 6

/* format */

console.log(`${ckb100.toString()} CKB is ${ckb100.toString(AmountUnit.shannon, {commify: true})} Shannon`);
// 100 CKB is 1,000,000 Shannon

console.log(`${shannon100.toString(AmountUnit.shannon)} Shannon is ${shannon100.toString()} CKB`)
// 100 Shannon is 0.000001 CKB

console.log(`${usdt.toString(6, {fixed: 2, commify: true})} USDT is rounded from ${usdt.toString(6, {pad: true})} USDT`);
// 1,234.57 USDT is rounded from 1234.567800 USDT


/* compare */

console.log('100 CKB is greater than 100 Shannon: ', ckb100.gt(shannon100));
console.log('100 CKB is less than 100 Shannon: ', ckb100.lt(shannon100));
// 100 CKB is greater than 100 Shannon: true
// 100 CKB is less than 100 Shannon: false

console.log('100 Shannon is equal to 0.000001 CKB: ', shannon100.eq(new Amount('0.000001')));
// 100 Shannon is equal to 0.000001 CKB: true


/* calculate */

console.log(`100 CKB + 100 Shannon = ${ckb100.add(shannon100).toString()} CKB`);
console.log(`100 CKB - 100 Shannon = ${ckb100.sub(shannon100).toString()} CKB`);
// 100 CKB + 100 Shannon = 100.000001 CKB
// 100 CKB - 100 Shannon = 99.999999 CKB

// Amount is assumed with unit, so if we want to perform multiplication or division, the best way is to convert the Amount instance to JSBI BigInt, and convert back to Amount instance if necessary.
const bn = JSBI.mul(ckb100.toBigInt(), JSBI.BigInt(10));
const amount = new Amount(bn.toString())
console.log(`100 CKB * 10 = ${amount.toString(AmountUnit.ckb, {commify: true})} CKB`);
// 100 CKB * 10 = 1,000 CKB

Cell

/* load from blockchain with a rpc instance and an outpoint */
const cell1 = Cell.loadFromBlockchain(rpc, outpoint);

/* convert rpc formated data to a Cell instance */
const cell2 = Cell.fromRPC(rpcData);

/* check how many capacity is occupied by scripts and data */
const occupiedCapacity = cell1.occupiedCapacity();

/* check if the cell's capacity is enough for the actual size */
cell1.spaceCheck();

/* check if the cell is empty (no data is stored) */
cell2.isEmpty()

/* adjust the capacity to the minimal value of this cell */
cell2.resize();

/* set / get amount of an sUDT cell */
const cell3 = cell2.clone();
cell3.setSUDTAmount(new Amount(100));
console.log('sUDT amount: ', cell3.getSUDTAmount().toString());
// sUDT amount: 100

/* playing with data */
cell1.setData('data');
cell2.setHexData('0x64617461');
console.log('data of cell 1: ', cell1.getData());
console.log('data of cell 2: ', cell1.getData());
console.log('hex data of cell 1: ', cell1.getHexData());
// data of cell 1: data
// data of cell 2: data
// hex data of cell 1: 0x64617461


Documents and Demos

Document Site

We will continue to improve the documentation according to our progress and feedbacks from developers.

Demos & Products

6 Likes