[翻译]:让 PW-Core 能够适配后端使用的提案(有 50000 CKB+ 的奖励!!)

背景

PW-Core (github)是个为前端设计的 sdk。有了 pw-core,您可以直接使用各种加密钱包作为 CKB 钱包。此外,我们提供了许多友好的模型和更原生的方式来建立您的交易。如果您是CKB dAp p开发的新手,请务必来尝试一下pw-core(可以先从我们的支付演示开始)。

最近,我们收到了在后端开发中使用 pw-core 来为 dApp 开发人员提供统一体验的要求,我们也认为这是一个好主意。然而,我们自己也缺乏工程师,没有时间去做适配。因此,我们提出了一个建议来让这些新增的组件可以添加到 pw-core 中以供后端使用。

RawProvider

在后端开发中,如果开发者需要用他的密钥签署一笔交易,他应该都要是自动完成的。所以,我们需要的提供者的最基本的一个功能是,它允许导入私钥并能够对交易进行签名。我们称之为RawProvider。RawProvider的 实现可能是这样的:

  • Constructor 中传递一个私钥字符串;
  • sign 方法中实现 CKB 的默认的签名方法(也就是使用内置的Blake2bSigner);

就这样,不是很难对吧?

IndexerCollector

另一个缺失的拼图是一个基于官方的 ckb-indexer的收集器
目前 pw-core 中的默认收集器是 PwCollector ,它基于 Portal Wallet 的后端服务器。对于开发人员来说,将 dApp 的数据服务依赖于其他项目的后端并不是一个好的选择,但是构建一个服务器来缓存和分析 CKB 的交易也是非常昂贵的。因此,最好的解决方案是构建一个适应 公共 ckb-indexer 服务 的收集器。

要构建 IdexerCollector,您需要读取 ckb-indexer 的 rpc 的方法,并将它们适配到收集器的接口中。您可以参考内置的 PwCollectorSUDTCollector 的实现。

来自 Lay2 的小福利

为了增加些让你开始 CKB 探险的激励,我们提供5万CKB(现在约等于 500美元 ,也许不久的将来会等值特斯拉 Model Y :stuck_out_tongue: )来奖励任何可以来实现这两个组件,并让你的 PR 被我们的开发团队接受的人。当然,如果你有任何问题,请随时在这里提出,我们也会邀请你加入我们的聊天群以获得更多的即时支持。

译者注:
【1】 Model Y
image
【2】还有大大继续加码,各位勇者们快来~

2 Likes

我运行Node项目十之八九都是编译不通过的(所以,不好测试):

所以在此针对 RawProvider简单参与一下。抛砖引玉~ :grinning_face_with_smiling_eyes:

1、更新pw-core/src/hashers/blake2b-hasher.ts的构造函数,以支持传入私钥,其它不变:

constructor(key: string = null) {
    const keyData = !key || key.length === 0 ? null : Buffer.from(key);
    const h = blake2b(
      32,
      keyData,
      null,
      new Uint8Array(Reader.fromRawString('ckb-default-hash').toArrayBuffer())
    );
    super(h);
  }

gist: https://gist.github.com/taurenshaman/86f96ae81a3f26188bffa735218a70c0

2、增加文件:pw-core/src/signers/blake2b-signer.ts

import { Signer, Message } from '.';
import { Blake2bHasher } from '../hashers';
import { Provider } from '../providers';

export class Blake2bSigner extends Signer {

  constructor(public readonly provider: Provider, key: string = null) {
    super(new Blake2bHasher(key));
  }

  signMessage(message: string): Promise<string>{
    return this.provider.sign(message);
  }

  async signMessages(messages: Message[]): Promise<string[]> {
    const sigs = [];
    for (const message of messages) {
      if (
        this.provider.address.toLockScript().toHash() === message.lock.toHash()
      ) {
        sigs.push(await this.provider.sign(message.message));
      } else {
        sigs.push('0x');
      }
    }

    return sigs;
  }

}

gist: https://gist.github.com/taurenshaman/ed4dfd5019ade10e78a2dbacf24ac955

3、增加文件:pw-core/src/providers/raw-provider.ts,基于DummyProvider修改:

import { Provider, Platform } from './provider';
import { Address, AddressType } from '..';
import { Blake2bSigner } from '../signers/blake2b-signer';

export class RawProvider extends Provider {
  secret: string;
  signer: Blake2bSigner;

  sign(message: string): Promise<string> {
    console.log('message', message);
    return this.signer.signMessage(message);
  }

  constructor(secret: string, platform: Platform = Platform.ckb) {
    super(platform);
    this.secret = secret;
    this.signer = new Blake2bSigner(this, secret);
  }

   async init(): Promise<Provider> {
    if (this.platform === Platform.eth) {
      this.address = new Address(
        '0x26C5F390FF2033CbB44377361c63A3Dd2DE3121d',
        AddressType.eth
      );
    } else {
      this.address = new Address(
        'ckt1qyqxpayn272n8km2k08hzldynj992egs0waqnr8zjs',
        AddressType.ckb
      );
    }
    return this;
  }
  
  async close() {
    console.log('do nothing');
  }
}

gist: https://gist.github.com/taurenshaman/de4cb3ce973ac53799723124cb1b703b

4 Likes

针对IndexerCollector 抛砖引玉~ :grinning_face_with_smiling_eyes:

  1. 实现getBalance。根据ckb-indexer,可以直接调用get_cells_capacity,输入是script和scrip_type,输出的capacity的单位应该是ckb。
// get capacity by lock script
  async getBalance(address: Address): Promise<Amount> {
    const data = {
      data: {
        script: address.toLockScript().serializeJson(),
        scrip_type: "lock",
      }
    };
    
    const res = await axios.post(
      `${
        this.apiBase
      }/get_cells_capacity`,
      data
    );
    /* result data: {
        capacity - total capacity
        block_hash - indexed tip block hash
        block_number - indexed tip block number
      }
    */
    //PwCollector: return new Amount(res.data.data, AmountUnit.shannon);
    return new Amount(res.data.capacity, AmountUnit.ckb);
  }
  1. 实现collect。根据含义应该是get unspent cells。可以根据上面的代码调用rpc的 get_cells
// get unspent cells
  async collect(address: Address, options: CollectorOptions): Promise<Cell[]> {
    if (!options || !options.neededAmount) {
      throw new Error("'neededAmount' in options must be provided");
    }
    const addressLockScript = address.toLockScript();
    const cells: Cell[] = [];
    // 怎么把capacity=${options.neededAmount.toHexString()}传进去?
    const reqData = {
      data: {
        script: address.toLockScript().serializeJson(),
        scrip_type: "lock",
      }
    };
    const res = await axios.post(
      `${
        this.apiBase
      }/get_cells`,
      reqData
    );
    /* result data: {
        objects - live cells
        last_cursor - pagination parameter
      }
    */
    for (let liveCell of res.data.objects) {
      // capacity = new Amount(capacity, AmountUnit.shannon);
      // outPoint = new OutPoint(outPoint.txHash, outPoint.index);
      //cells.push(new Cell(capacity, addressLockScript, null, outPoint));
      cells.push(Cell.fromRPC(liveCell)); // 应该可以直接fromRPC吧?
    }
    return cells;
  }
  1. 浏览时从ckb-js-toolkit中发现了RPC,尝试直接调用rpc.collect如下,最后对数据遍历时,没有找到怎么获取capacity:
constructor(public apiBase: string = "https://mainnet.ckb.dev/indexer") {
    super();
    this.apiBase = apiBase;
    this.rpc = new RPC(this.apiBase);
  }

// get unspent cells
  async collect(address: Address, options: CollectorOptions): Promise<Cell[]> {
    if (!options || !options.neededAmount) {
      throw new Error("'neededAmount' in options must be provided");
    }
    const addressLockScript = address.toLockScript();
    const rpcc = new cell_collectors.RPCCollector(this.rpc, addressLockScript.toHash(), {
      skipCellWithContent: true,
      loadData: true
    });
    const data = rpcc.collect();
    const cells: Cell[] = [];

    (async () => {
        for await (const x of data) {
          // 这里的capacity怎么读取?
          const cap= new Amount(capacity, AmountUnit.shannon);
          const outPoint = OutPoint.fromRPC(x.data);
          cells.push(new Cell(cap, addressLockScript, null, outPoint));
        }
    })();
    return cells;
  }
  1. 找资料时,找到了 CKBRPC in ckb-sdk-core
    当前项目中没找到@nervosnetwork/ckb-sdk-core,尝试安装,失败。
    CKBRPC中的getCapacityByLockHash应该就是IndexerCollector的getBalance需要的方法;没有找到可直接用的通过neededAmount查询的方法。
    不知道为什么没有引入@nervosnetwork/ckb-sdk-core,有了这个库,就不需要通过axios进行查询了。

  2. 关于如何实现collect的一个思路。
    在wile(true)循环中,可以通过第2条或者CKBRPC. getLiveCellsByLockHash获取一个分页的cells,然后比较neededAmount和分页中所有cell的capacity之和:小于,取分页中最小够用的cells集合,跳出循环;大于,继续获取下一个分页的数据。

const resultCells = [];
while(true){
   const cells = ..... // 第2条的axios调用,或者CKBRPC. getLiveCellsByLockHash
   const totalCapacity = ... // 遍历cells数组计算capacity之和
   if(totalCapacity >= neededAmount){
      // cells --> resultCells: 可能只需要cells中的一部分而非全部
      break;
   }
}
2 Likes

不错的实现,有两个地方可以改进一下:

  1. hasher这个最初的设计是用来做hash的,hash算法不涉及private key,这个地方的修改是没有必要的
  1. 可以将使用privatekey对message进行签名的逻辑在signMessage中实现。方便raw-provider调用blake2bSigner完成签名操作。
2 Likes

实验性地实现了一版

底下是测试用的代码

const {
  default: PWCore,
  AddressType,
  Address,
  Amount,
  DefaultSigner,
  Blake2bHasher,
} = require('./build/main');
const { RawProvider } = require('./build/main/providers/raw-provider');
const { SimpleBuilder } = require('./build/main/builders/simple-builder');
const {
  IndexerCollector,
} = require('./build/main/collectors/indexer-collector');

async function main() {
  const pw = new PWCore('https://aggron.ckb.dev');
  await pw.init(
    new RawProvider(
      '0xprivate key'
    ),
    new IndexerCollector()
  );

  const builder = new SimpleBuilder(
    new Address(
      'ckt...',
      AddressType.ckb
    ),
    new Amount('100')
  );

  const tx = await builder.build();

  const signer = new DefaultSigner(PWCore.provider);
  signer.hasher = new Blake2bHasher();

  const txHash = await pw.sendTransaction(tx, signer);

  console.log(txHash);
}

main();

3 Likes