Skip to content

Commit

Permalink
✨ Import multisig address from JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
doitian committed Feb 26, 2024
1 parent eb4c03a commit 0a0d415
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 4 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"autoprefixer": "^10.4.17",
"bech32": "^2.0.0",
"flowbite-react": "^0.7.2",
"immer": "^10.0.3",
"postcss": "^8.4.35",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import AddressPage from "./AddressPage.js";
import IndexPage from "./IndexPage.js";
import Layout from "./Layout.js";
import NewAddressPage from "./NewAddressPage.js";
import ImportAddressPage from "./ImportAddressPage.js";
import usePersistReducer from "./reducer.js";

function App() {
Expand All @@ -30,6 +31,9 @@ function Router() {
const staticRoutes = {
"#/": () => <IndexPage {...{ navigate, state, deleteAddress }} />,
"#/addresses/new": () => <NewAddressPage {...{ navigate, addAddress }} />,
"#/addresses/import": () => (
<ImportAddressPage {...{ navigate, addAddress }} />
),
};
staticRoutes[""] = staticRoutes["#/"];
const dynamicRoutes = [
Expand Down
83 changes: 83 additions & 0 deletions src/ImportAddressPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Alert, Button, FileInput } from "flowbite-react";
import { useState } from "react";
import { importMultisigAddresses } from "./lib/multisig-address.js";

const ERROR_MESSAGE = "Opps, error occurs when processing the uploaded file";

export default function NewAddressPage({ addAddress, navigate }) {
const [state, setState] = useState({
isProcessing: false,
error: null,
});

const submit = (e) => {
e.preventDefault();
setState({ isProcessing: true, error: null });

try {
const reader = new FileReader();

reader.addEventListener("load", (loaded) => {
try {
const addresses = importMultisigAddresses(
JSON.parse(loaded.target.result),
);
addAddress(addresses);
navigate("#/");
} catch (error) {
setState({
isProcessing: false,
error: `${ERROR_MESSAGE}: ${error}`,
});
}
});
reader.addEventListener("error", () => {
setState({ isProcessing: false, error: ERROR_MESSAGE });
});
reader.addEventListener("abort", () => {
setState({ isProcessing: false, error: "Uploading aborted" });
});

const fileInput = document.getElementById("file-upload");
reader.readAsText(fileInput.files[0]);
} catch (error) {
setState({
isProcessing: false,
error: `${ERROR_MESSAGE}: ${error}`,
});
}
};

return (
<form onSubmit={submit} className="flex flex-col gap-4">
<h2 className="text-lg mb-4">Import Multisig Addresses</h2>
{state.error ? (
<Alert className="mb-4" color="failure">
{state.error}
</Alert>
) : null}

<div>
<p className="mb-0">Supported formats:</p>
<ul className="mb-4 list-disc list-inside">
<li>
The JSON file generated by <code>ckb-cli tx init</code>
</li>
<li>
The JSON file exported from Neuron via the Multisig Address Tool in
the Tools menu.
</li>
</ul>
<FileInput id="file-upload" name="file-upload" required />
</div>

<Button
isProcessing={state.isProcessing}
disabled={state.isProcessing}
type="submit"
>
Upload
</Button>
</form>
);
}
9 changes: 7 additions & 2 deletions src/IndexPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function AddressesList({ navigate, addresses, deleteAddress }) {
<ul className="mb-4">
{addresses.map((address) => (
<li
key={`address-#{address.args}`}
key={`address-${address.args}`}
className="font-mono flex flex-row gap-2 items-center p-2 hover:bg-slate-100"
>
<a className="grow break-all" href={`#/addresses/${address.args}`}>
Expand All @@ -18,7 +18,12 @@ function AddressesList({ navigate, addresses, deleteAddress }) {
</li>
))}
</ul>
<Button onClick={() => navigate("#/addresses/new")}>Add Address</Button>
<div className="mb-4 flex flex-row gap-2 flex-wrap">
<Button onClick={() => navigate("#/addresses/new")}>Add Address</Button>
<Button onClick={() => navigate("#/addresses/import")}>
Import Addresses
</Button>
</div>
</section>
);
}
Expand Down
25 changes: 25 additions & 0 deletions src/lib/ckb-address.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { encodeCkbAddress, decodeCkbAddress } from "@ckb-cobuild/ckb-address";
import { ckbHasher } from "@ckb-cobuild/ckb-hasher";
import { decodeHex } from "@ckb-cobuild/hex-encoding";
import { bech32 } from "bech32";

export { encodeCkbAddress, decodeCkbAddress };

export function arrayEqual(a, b) {
if (a.length === b.length) {
Expand Down Expand Up @@ -68,3 +71,25 @@ export function generateMultisigAddress(config, prefix) {

return encodeCkbAddress(script, prefix);
}

export function decodeDeprecatedSecp256k1Address(address) {
const { prefix, words } = bech32.decode(address, 1023);
if (prefix !== "ckb" && prefix !== "ckt") {
return;
}

const [formatType, ...body] = bech32.fromWords(words);

// payload = 0x01 | code_hash_index | args
if (formatType === 1) {
const [shortId, ...args] = body;
// secp256k1
if (shortId === 0 && args.length === 20) {
return {
code_hash: SECP256K1_CODE_HASH,
hash_type: "type",
args: Uint8Array.from(args),
};
}
}
}
36 changes: 36 additions & 0 deletions src/lib/multisig-address.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { MultisigConfig } from "../schemas.js";
import {
decodeDeprecatedSecp256k1Address,
encodeCkbAddress,
} from "./ckb-address.js";

export function convertDeprecatedSecp256k1Address(addresses) {
return addresses.map((address) => {
const decoded = decodeDeprecatedSecp256k1Address(address);
return decoded === undefined
? address
: encodeCkbAddress(decoded, address.slice(0, 3));
});
}

export function importMultisigAddresses(jsonContent) {
const found = [];

if ("multisig_configs" in jsonContent) {
for (const [args, value] of Object.entries(jsonContent.multisig_configs)) {
found.push(
MultisigConfig.parse({
args,
signers: convertDeprecatedSecp256k1Address(value.sighash_addresses),
required: value.require_first_n,
threshold: value.threshold,
}),
);
}
}

if (found.length === 0) {
throw new Error("No multisg configs found in the file");
}
return found;
}
9 changes: 7 additions & 2 deletions src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ function deleteAddressByArgs(draft, args) {
function reducer(draft, action) {
switch (action.type) {
case "addAddress":
deleteAddressByArgs(draft, action.payload.args);
draft.addresses.push(action.payload);
const addresses = Array.isArray(action.payload)
? action.payload
: [action.payload];
for (const address of addresses) {
deleteAddressByArgs(draft, address.args);
draft.addresses.push(address);
}
break;
case "deleteAddress":
deleteAddressByArgs(draft, action.payload);
Expand Down

0 comments on commit 0a0d415

Please sign in to comment.