Skip to content

Commit

Permalink
feat(config-manager): supported refresh deployment config
Browse files Browse the repository at this point in the history
  • Loading branch information
homura committed Jan 12, 2024
1 parent 349be74 commit 8cfaf8f
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/grumpy-plants-pretend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ckb-lumos/config-manager": minor
---

feat: `refreshScriptConfigs` to refresh deployment config
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@devtools/changelog": "workspace:^",
"@strictsoftware/typedoc-plugin-monorepo": "^0.3.1",
"@types/node": "^20.1.0",
"@types/sinon": "^17.0.3",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"ava": "^3.8.2",
Expand Down
1 change: 1 addition & 0 deletions packages/config-manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@ckb-lumos/base": "0.22.0-next.2",
"@ckb-lumos/bi": "0.22.0-next.2",
"@ckb-lumos/codec": "0.22.0-next.2",
"@ckb-lumos/rpc": "0.22.0-next.2",
"@types/deep-freeze-strict": "^1.1.0",
"deep-freeze-strict": "^1.1.1"
},
Expand Down
164 changes: 164 additions & 0 deletions packages/config-manager/src/refresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import type {
OutPoint,
Script,
Transaction,
TransactionWithStatus,
} from "@ckb-lumos/base";
import type { ScriptConfig, ScriptConfigs } from "./types";
import type { RPC } from "@ckb-lumos/rpc";
import type { CKBComponents } from "@ckb-lumos/rpc/lib/types/api";

type MaybePromise<T> = T | PromiseLike<T>;

type LatestOutpointResolver = (
outPoints: OutPoint[]
) => MaybePromise<OutPoint[]>;

type FetchTxs = (txHashes: string[]) => MaybePromise<Transaction[]>;
type FetchTypeIdCells = (
scripts: Script[]
) => MaybePromise<CKBComponents.IndexerCellWithoutData[]>;

/* c8 ignore next 39 */
export function createRpcResolver(rpc: RPC): LatestOutpointResolver {
const fetchTxs: FetchTxs = async (txHashes) => {
const txs: TransactionWithStatus[] = await rpc
.createBatchRequest(txHashes.map((txHash) => ["getTransaction", txHash]))
.exec();

return zipWith(txHashes, txs, (txHash, tx) => {
if (!tx?.transaction) {
throw new Error(`Cannot find transaction ${txHash}`);
}
return tx.transaction;
});
};

const fetchIndexerCells: FetchTypeIdCells = async (typeIds) => {
const res: CKBComponents.GetLiveCellsResult<false>[] = await rpc
.createBatchRequest(
typeIds.map((typeId) => [
"getCells",
{
script: typeId,
scriptType: "type",
scriptSearchMode: "exact",
withData: false,
} satisfies CKBComponents.GetCellsSearchKey<false>,
"asc" satisfies CKBComponents.Order,
"0x1" satisfies CKBComponents.UInt64,
])
)
.exec();

return res.map<CKBComponents.IndexerCellWithoutData>(
(item) => item.objects[0]
);
};

return createResolver(fetchTxs, fetchIndexerCells);
}

export function createResolver(
fetchTxs: FetchTxs,
fetchTypeScriptCell: FetchTypeIdCells
): LatestOutpointResolver {
return async (oldOutPoints) => {
const txs = await fetchTxs(oldOutPoints.map((outPoint) => outPoint.txHash));

const typeScripts = zipWith(oldOutPoints, txs, (outPoint, tx) => {
nonNullable(outPoint);

nonNullable(
tx,
`Cannot find the OutPoint ${outPoint.txHash}#${outPoint.index}`
);

return tx.outputs[Number(outPoint.index)].type;
});

const cells = await fetchTypeScriptCell(
typeScripts.filter(Boolean) as Script[]
);

return zipWith(oldOutPoints, typeScripts, (oldOutPoint, script) => {
nonNullable(oldOutPoint);
if (!script) {
return oldOutPoint;
}

const [cell] = cells.splice(0, 1);
return cell.outPoint;
});
};
}

Check warning on line 94 in packages/config-manager/src/refresh.ts

View check run for this annotation

Codecov / codecov/patch

packages/config-manager/src/refresh.ts#L62-L94

Added lines #L62 - L94 were not covered by tests

type RefreshConfig<S> = {
resolve: LatestOutpointResolver;
skip?: (keyof S)[];
};

/**
* Refreshing the config items in {@link ScriptConfigs} which are deployed with type id
* @example
* const updatedScriptConfigs = upgrade(predefined.AGGRON4.SCRIPTS, createRpcResolver(rpc))
* initializeConfig({ SCRIPTS: updatedScriptConfigs })
*/
export async function refreshScriptConfigs<S extends ScriptConfigs>(
scriptConfigs: S,
{
resolve,
skip = ["SECP256K1_BLAKE160", "SECP256K1_BLAKE160_MULTISIG", "DAO"],
}: RefreshConfig<S>
): Promise<S> {
// prettier-ignore
type Filter = (value: [string, ScriptConfig | undefined]) => value is [string, ScriptConfig];

const configs = Object.entries(scriptConfigs).filter(
(([name, scriptConfig]) =>
!skip.includes(name) && scriptConfig?.HASH_TYPE === "type") as Filter
);

const oldOutPoints: OutPoint[] = configs.map(([_, scriptConfig]) => ({
txHash: scriptConfig.TX_HASH,
index: scriptConfig.INDEX,
}));

const newOutPoints: OutPoint[] = await resolve(oldOutPoints);

const newScriptConfigs = Object.fromEntries(
zipWith(configs, newOutPoints, (target, newOutPoint) => {
nonNullable(target);
const [name, original] = target;

nonNullable(
newOutPoint,
`Refreshing failed, cannot load config of ${name}, please check whether the scriptConfig is correct`
);

return [
name,
{ ...original, TX_HASH: newOutPoint.txHash, INDEX: newOutPoint.index },
];
})
);

return Object.assign({}, scriptConfigs, newScriptConfigs);
}

function zipWith<A, B, T>(
a: A[],
b: B[],
cb: (a: A | undefined, b: B | undefined) => T
) {
return a.map((_, i) => cb(a[i], b[i]));
}

function nonNullable<T>(
t: T,
message = "Not nullable"
): asserts t is NonNullable<T> {
if (t == null) {
throw new Error(message);
}

Check warning on line 163 in packages/config-manager/src/refresh.ts

View check run for this annotation

Codecov / codecov/patch

packages/config-manager/src/refresh.ts#L162-L163

Added lines #L162 - L163 were not covered by tests
}
60 changes: 60 additions & 0 deletions packages/config-manager/tests/refresh.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import test from "ava";
import { refreshScriptConfigs } from "../src/refresh";
import { ScriptConfigs } from "../src";
import { hexify } from "@ckb-lumos/codec/lib/bytes";
import { randomBytes } from "node:crypto";

function randomHash() {
return hexify(randomBytes(32));
}

test("refresh without update", async (t) => {
const scriptConfigs: ScriptConfigs = {
A: {
CODE_HASH: randomHash(),
TX_HASH: randomHash(),
HASH_TYPE: "type",
DEP_TYPE: "code",
INDEX: "0x1",
},
B: {
CODE_HASH: randomHash(),
TX_HASH: randomHash(),
HASH_TYPE: "type",
DEP_TYPE: "code",
INDEX: "0x1",
},
};
const refreshed1 = await refreshScriptConfigs(scriptConfigs, {
resolve: (outPoints) => outPoints,
});

t.deepEqual(refreshed1, scriptConfigs);
});

test("refresh with skip", async (t) => {
const scriptConfigs: ScriptConfigs = {
A: {
CODE_HASH: randomHash(),
TX_HASH: randomHash(),
HASH_TYPE: "type",
DEP_TYPE: "code",
INDEX: "0x1",
},
B: {
CODE_HASH: randomHash(),
TX_HASH: randomHash(),
HASH_TYPE: "type",
DEP_TYPE: "code",
INDEX: "0x1",
},
};
const refreshed2 = await refreshScriptConfigs(scriptConfigs, {
resolve: async (outPoints) =>
outPoints.map(() => ({ txHash: randomHash(), index: "0x0" })),
skip: ["B"],
});

t.notDeepEqual(scriptConfigs.A, refreshed2.A);
t.deepEqual(scriptConfigs.B, refreshed2.B);
});
14 changes: 13 additions & 1 deletion pnpm-lock.yaml

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

0 comments on commit 8cfaf8f

Please sign in to comment.