Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: supported bitcoin auth in omnilock #607

Merged
merged 17 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/flat-feet-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ckb-lumos/debugger": minor
---

feat: update default ckb-debugger version to 0.112
5 changes: 5 additions & 0 deletions .changeset/gold-seals-pretend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ckb-lumos/common-scripts": minor
---

feat: supported bitcoin auth in omnilock
10 changes: 10 additions & 0 deletions examples/misc-nodejs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@lumos-example/misc",
"private": true,
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@ckb-lumos/lumos": "canary",
"@ckb-lumos/debugger": "canary"
}
}
64 changes: 64 additions & 0 deletions examples/misc-nodejs/verify-sig-via-debugger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { randomBytes } from "crypto";
import { config, hd, Script } from "@ckb-lumos/lumos";
import { CKBDebuggerDownloader, createTestContext, getDefaultConfig } from "@ckb-lumos/debugger";
import { TransactionSkeleton } from "@ckb-lumos/lumos/helpers";
import { common } from "@ckb-lumos/lumos/common-scripts";
import { blockchain, bytes } from "@ckb-lumos/lumos/codec";
import { computeScriptHash } from "@ckb-lumos/lumos/utils";
import { mockOutPoint } from "@ckb-lumos/debugger/lib/context";

const context = createTestContext(getDefaultConfig());
const lumosConfig = { PREFIX: "ckt", SCRIPTS: context.scriptConfigs };
config.initializeConfig(lumosConfig);

const privateKey = bytes.hexify(randomBytes(32));

async function main() {
await new CKBDebuggerDownloader().downloadIfNotExists();

const txSkeleton = TransactionSkeleton().asMutable();
const lock: Script = {
codeHash: context.scriptConfigs.SECP256K1_BLAKE160.CODE_HASH,
hashType: context.scriptConfigs.SECP256K1_BLAKE160.HASH_TYPE,
args: hd.key.privateKeyToBlake160(privateKey),
};

await common.setupInputCell(txSkeleton, {
cellOutput: { capacity: "0x123123123", lock: lock },
data: "0x",
outPoint: mockOutPoint(),
});

txSkeleton.update("cellDeps", (cellDeps) =>
cellDeps.push({
depType: context.scriptConfigs.SECP256K1_BLAKE160.DEP_TYPE,
outPoint: {
txHash: context.scriptConfigs.SECP256K1_BLAKE160.TX_HASH,
index: context.scriptConfigs.SECP256K1_BLAKE160.INDEX,
},
})
);

common.prepareSigningEntries(txSkeleton, { config: lumosConfig });
const message = txSkeleton.get("signingEntries").get(0)!.message;
const sig = hd.key.signRecoverable(message, privateKey);
txSkeleton.update("witnesses", (witnesses) =>
witnesses.update(0, (witness) => {
const unsigned = blockchain.WitnessArgs.unpack(witness!);
unsigned.lock = sig;
return bytes.hexify(blockchain.WitnessArgs.pack(unsigned));
})
);

const result = await context.executor.execute(txSkeleton, {
scriptHash: computeScriptHash(lock),
scriptGroupType: "lock",
});

console.assert(result.code === 0);

console.log("Cycles consumed", result.cycles);
console.log(result.message);
}

main();
21 changes: 21 additions & 0 deletions examples/omni-lock-unisat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Lumos Works with UniSat

Nervos maintains a powerful lock
called [Omni Lock](https://github.com/XuJiandong/docs-bank/blob/master/omni_lock.md) (previously named RC lock), which
Keith-CY marked this conversation as resolved.
Show resolved Hide resolved
can use MetaMask as a signer. This example will show how to use Lumos to send a transaction using Omni lock and MetaMask
Keith-CY marked this conversation as resolved.
Show resolved Hide resolved

## Quick Start

> we should [build](..) Lumos project first before we start this example

```
npm run build
cd examples/omni-lock-
npm start
```

## Links

- [MetaMask](https://unisat.io/) - A crypto wallet for Bitcoin
- [Nervos Faucet](https://faucet.nervos.org/) - Claim Nervos testnet CKB
- [Omnilock](https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0042-omnilock/0042-omnilock.md) - Omni lock intro
13 changes: 13 additions & 0 deletions examples/omni-lock-unisat/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lumos ❤️ UniSat</title>
</head>
<body>
<div id="root"></div>
<script src="index.tsx" type="module"></script>
</body>
</html>
95 changes: 95 additions & 0 deletions examples/omni-lock-unisat/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, { useEffect, useState } from "react";
import { helpers, Script, config, commons } from "@ckb-lumos/lumos";
import ReactDOM from "react-dom";
import { asyncSleep, capacityOf, unisat, transfer } from "./lib";

// use testnet config
config.initializeConfig(config.predefined.AGGRON4);

const App: React.FC = () => {
const [bitcoinAddr, setBitcoinAddr] = useState("");
const [omniAddr, setOmniAddr] = useState("");
const [omniLock, setOmniLock] = useState<Script>();
const [balance, setBalance] = useState("-");

const [transferAddr, setTransferAddress] = useState("");
const [transferAmount, setTransferAmount] = useState("");

const [isSendingTx, setIsSendingTx] = useState(false);
const [txHash, setTxHash] = useState("");

useEffect(() => {
asyncSleep(300).then(() => {
if (unisat) connectToWallet();
});
}, []);

function connectToWallet() {
unisat
.requestAccounts()
.then(([bitcoinAddr]: string[]) => {
const omniLockScript = commons.omnilock.createOmnilockScript({
auth: { flag: "BITCOIN", address: bitcoinAddr },
});

const omniAddr = helpers.encodeToAddress(omniLockScript);

setBitcoinAddr(bitcoinAddr);
setOmniAddr(omniAddr);
setOmniLock(omniLockScript);

return omniAddr;
})
.then((omniAddr) => capacityOf(omniAddr))
.then((balance) => setBalance(balance.div(10 ** 8).toString() + " CKB"));
}

function onTransfer() {
if (isSendingTx) return;
setIsSendingTx(true);

transfer({ amount: transferAmount, from: omniAddr, to: transferAddr })
.then(setTxHash)
.catch((e) => {
console.log(e);
alert(e.message || JSON.stringify(e));
})
.finally(() => setIsSendingTx(false));
}

if (!unisat) return <div>UniSat is not installed</div>;
if (!bitcoinAddr) return <button onClick={connectToWallet}>Connect to UniSat</button>;

return (
<div>
<ul>
<li>Bitcoin Address: {bitcoinAddr}</li>
<li>Nervos Address(Omni): {omniAddr}</li>
<li>
Current Omni lock script:
<pre>{JSON.stringify(omniLock, null, 2)}</pre>
</li>

<li>Balance: {balance}</li>
</ul>

<div>
<h2>Transfer to</h2>
<label htmlFor="address">Address</label>&nbsp;
<input id="address" type="text" onChange={(e) => setTransferAddress(e.target.value)} placeholder="ckt1..." />
<br />
<label htmlFor="amount">Amount</label>
&nbsp;
<input id="amount" type="text" onChange={(e) => setTransferAmount(e.target.value)} placeholder="shannon" />
<br />
<button onClick={onTransfer} disabled={isSendingTx}>
Transfer
</button>
<p>Tx Hash: {txHash}</p>
</div>
</div>
);
};

const app = document.getElementById("root");
ReactDOM.render(<App />, app);
134 changes: 134 additions & 0 deletions examples/omni-lock-unisat/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { BI, Cell, helpers, Indexer, RPC, config, commons } from "@ckb-lumos/lumos";
import { omnilock } from "@ckb-lumos/lumos/common-scripts";
import { blockchain, bytify, hexify } from "@ckb-lumos/lumos/codec";

const CKB_RPC_URL = "https://testnet.ckb.dev";
const rpc = new RPC(CKB_RPC_URL);
const indexer = new Indexer(CKB_RPC_URL);

declare global {
interface Window {
unisat: omnilock.bitcoin.Provider;
}
}

export const unisat = window.unisat;

export function asyncSleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

interface Options {
from: string;
to: string;
amount: string;
}
const SECP_SIGNATURE_PLACEHOLDER = hexify(
new Uint8Array(
commons.omnilock.OmnilockWitnessLock.pack({
signature: new Uint8Array(65).buffer,
}).byteLength
)
);

export async function transfer(options: Options): Promise<string> {
const CONFIG = config.getConfig();
let tx = helpers.TransactionSkeleton({});
const fromScript = helpers.parseAddress(options.from);
const toScript = helpers.parseAddress(options.to);

// additional 0.001 ckb for tx fee
// the tx fee could calculate by tx size
// this is just a simple example
const neededCapacity = BI.from(options.amount).add(100000);
let collectedSum = BI.from(0);
const collectedCells: Cell[] = [];
const collector = indexer.collector({ lock: fromScript, type: "empty" });
for await (const cell of collector.collect()) {
collectedSum = collectedSum.add(cell.cellOutput.capacity);
collectedCells.push(cell);
if (BI.from(collectedSum).gte(neededCapacity)) break;
}

if (collectedSum.lt(neededCapacity)) {
throw new Error(`Not enough CKB, expected: ${neededCapacity}, actual: ${collectedSum} `);
}

const transferOutput: Cell = {
cellOutput: {
capacity: BI.from(options.amount).toHexString(),
lock: toScript,
},
data: "0x",
};

const changeOutput: Cell = {
cellOutput: {
capacity: collectedSum.sub(neededCapacity).toHexString(),
lock: fromScript,
},
data: "0x",
};

tx = tx.update("inputs", (inputs) => inputs.push(...collectedCells));
tx = tx.update("outputs", (outputs) => outputs.push(transferOutput, changeOutput));
tx = tx.update("cellDeps", (cellDeps) =>
cellDeps.push(
// omni lock dep
{
outPoint: {
txHash: CONFIG.SCRIPTS.OMNILOCK.TX_HASH,
index: CONFIG.SCRIPTS.OMNILOCK.INDEX,
},
depType: CONFIG.SCRIPTS.OMNILOCK.DEP_TYPE,
},
// SECP256K1 lock is depended by omni lock
{
outPoint: {
txHash: CONFIG.SCRIPTS.SECP256K1_BLAKE160.TX_HASH,
index: CONFIG.SCRIPTS.SECP256K1_BLAKE160.INDEX,
},
depType: CONFIG.SCRIPTS.SECP256K1_BLAKE160.DEP_TYPE,
}
)
);

const witness = hexify(blockchain.WitnessArgs.pack({ lock: SECP_SIGNATURE_PLACEHOLDER }));

// fill txSkeleton's witness with placeholder
for (let i = 0; i < tx.inputs.toArray().length; i++) {
tx = tx.update("witnesses", (witnesses) => witnesses.push(witness));
}

tx = commons.omnilock.prepareSigningEntries(tx, { config: CONFIG });

const signedMessage = await omnilock.bitcoin.signMessage(tx.signingEntries.get(0).message, "ecdsa", window.unisat);

const signedWitness = hexify(
blockchain.WitnessArgs.pack({
lock: commons.omnilock.OmnilockWitnessLock.pack({
signature: bytify(signedMessage).buffer,
}),
})
);

tx = tx.update("witnesses", (witnesses) => witnesses.set(0, signedWitness));

const signedTx = helpers.createTransactionFromSkeleton(tx);
const txHash = await rpc.sendTransaction(signedTx, "passthrough");

return txHash;
}

export async function capacityOf(address: string): Promise<BI> {
const collector = indexer.collector({
lock: helpers.parseAddress(address),
});

let balance = BI.from(0);
for await (const cell of collector.collect()) {
balance = balance.add(cell.cellOutput.capacity);
}

return balance;
}
22 changes: 22 additions & 0 deletions examples/omni-lock-unisat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"private": true,
"name": "@lumos-examples/omni-lock-unisat",
"description": "",
"scripts": {
"start": "parcel index.html",
"lint": "tsc --noEmit"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"@ckb-lumos/lumos": "canary",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"parcel": "^2.9.3"
}
}
9 changes: 9 additions & 0 deletions examples/omni-lock-unisat/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"lib": ["dom"],
"jsx": "react",
"module": "CommonJS",
"skipLibCheck": true,
"esModuleInterop": true
}
}
Loading
Loading