Skip to content

Commit

Permalink
Merge pull request #155 from ckb-ecofund/v0.3.x
Browse files Browse the repository at this point in the history
feat: merge v0.3.0-rc4
  • Loading branch information
RetricSu authored Oct 16, 2024
2 parents af07898 + 884fccd commit 72c1322
Show file tree
Hide file tree
Showing 13 changed files with 462 additions and 229 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = {
rules: {
// Other rules...
"@typescript-eslint/no-var-requires": "off",
'no-constant-condition': 'off',
"@typescript-eslint/no-explicit-any": "warn",
'@typescript-eslint/no-unused-vars': [
'error', // or 'off' to disable entirely
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,12 @@ To deploy the script, cd into the frontend folder and run:
cd frontend && offckb deploy --network <devnet/testnet>
```

Pass `--type-id` option if you want Scripts to be upgradable

```sh
cd frontend && offckb deploy --type-id --network <devnet/testnet>
```

Once the deployment is done, you can use the following command to check the deployed scripts:

```sh
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"typescript": "^5.3.3"
},
"dependencies": {
"@ckb-ccc/core": "^0.0.11-alpha.3",
"@ckb-ccc/core": "^0.0.16-alpha.3",
"@ckb-lumos/lumos": "0.23.0",
"@iarna/toml": "^2.2.5",
"@inquirer/prompts": "^4.1.0",
Expand Down
1 change: 1 addition & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ program
.description('Deploy contracts to different networks, only supports devnet and testnet')
.option('--network <network>', 'Specify the network to deploy to', 'devnet')
.option('--target <target>', 'Specify the relative bin target folder to deploy to')
.option('-t, --type-id', 'Specify if use upgradable type id to deploy the script')
.option('--privkey <privkey>', 'Specify the private key to deploy scripts')
.action((options: DeployOptions) => deploy(options));

Expand Down
132 changes: 9 additions & 123 deletions src/cmd/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,32 @@
import { commons, hd, helpers } from '@ckb-lumos/lumos';
import fs from 'fs';
import { NetworkOption, Network } from '../util/type';
import path from 'path';
import { Account, CKB } from '../util/ckb';
import { deployerAccount } from '../cfg/account';
import { listBinaryFilesInFolder, readFileToUint8Array, isAbsolutePath } from '../util/fs';
import { listBinaryFilesInFolder, isAbsolutePath } from '../util/fs';
import { validateNetworkOpt, validateExecDappEnvironment } from '../util/validator';
import { DeploymentOptions, generateDeploymentToml } from '../deploy/toml';
import { DeploymentRecipe, generateDeploymentRecipeJsonFile } from '../deploy/migration';
import { ckbHash, computeScriptHash } from '@ckb-lumos/lumos/utils';
import { genMyScriptsJsonFile } from '../scripts/gen';
import { OffCKBConfigFile } from '../template/offckb-config';
import { deployBinaries, getToDeployBinsPath, recordDeployResult } from '../deploy';
import { CKB } from '../sdk/ckb';

export interface DeployOptions extends NetworkOption {
target: string | null | undefined;
privkey?: string | null;
typeId?: boolean;
}

export async function deploy(opt: DeployOptions = { network: Network.devnet, target: null }) {
export async function deploy(opt: DeployOptions = { network: Network.devnet, typeId: false, target: null }) {
const network = opt.network as Network;
validateNetworkOpt(network);

const ckb = new CKB(network);
const ckb = new CKB({ network });

// we use deployerAccount to deploy contract by default
const privateKey = opt.privkey || deployerAccount.privkey;
const lumosConfig = ckb.getLumosConfig();
const from = CKB.generateAccountFromPrivateKey(privateKey, lumosConfig);

const enableTypeId = opt.typeId ?? false;
const targetFolder = opt.target;
if (targetFolder) {
const binFolder = isAbsolutePath(targetFolder) ? targetFolder : path.resolve(process.cwd(), targetFolder);
const bins = listBinaryFilesInFolder(binFolder);
const binPaths = bins.map((bin) => path.resolve(binFolder, bin));
const results = await deployBinaries(binPaths, from, ckb);
const results = await deployBinaries(binPaths, privateKey, enableTypeId, ckb);

// record the deployed contract infos
recordDeployResult(results, network, false); // we don't update my-scripts.json since we don't know where the file is
Expand All @@ -49,115 +42,8 @@ export async function deploy(opt: DeployOptions = { network: Network.devnet, tar

// read contract bin folder
const bins = getToDeployBinsPath();
const results = await deployBinaries(bins, from, ckb);
const results = await deployBinaries(bins, privateKey, enableTypeId, ckb);

// record the deployed contract infos
recordDeployResult(results, network);
}

function getToDeployBinsPath() {
const userOffCKBConfigPath = path.resolve(process.cwd(), 'offckb.config.ts');
const fileContent = fs.readFileSync(userOffCKBConfigPath, 'utf-8');
const match = fileContent.match(/contractBinFolder:\s*['"]([^'"]+)['"]/);
if (match && match[1]) {
const contractBinFolderValue = match[1];
const binFolderPath = isAbsolutePath(contractBinFolderValue)
? contractBinFolderValue
: path.resolve(process.cwd(), contractBinFolderValue);
const bins = listBinaryFilesInFolder(binFolderPath);
return bins.map((bin) => path.resolve(binFolderPath, bin));
} else {
console.log('contractBinFolder value not found in offckb.config.ts');
return [];
}
}

type DeployBinaryReturnType = ReturnType<typeof deployBinary>;
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type DeployedInterfaceType = UnwrapPromise<DeployBinaryReturnType>;

async function recordDeployResult(results: DeployedInterfaceType[], network: Network, updateMyScriptsJsonFile = true) {
if (results.length === 0) {
return;
}
for (const result of results) {
generateDeploymentToml(result.deploymentOptions, network);
generateDeploymentRecipeJsonFile(result.deploymentOptions.name, result.deploymentRecipe, network);
}

// update my-scripts.json
if (updateMyScriptsJsonFile) {
const userOffCKBConfigPath = path.resolve(process.cwd(), 'offckb.config.ts');
const folder = OffCKBConfigFile.readContractInfoFolder(userOffCKBConfigPath);
if (folder) {
const myScriptsFilePath = path.resolve(folder, 'my-scripts.json');
genMyScriptsJsonFile(myScriptsFilePath);
}
}

console.log('done.');
}

async function deployBinaries(binPaths: string[], from: Account, ckb: CKB) {
if (binPaths.length === 0) {
console.log('No binary to deploy.');
}
const results: DeployedInterfaceType[] = [];
for (const bin of binPaths) {
const result = await deployBinary(bin, from, ckb);
results.push(result);
}
return results;
}

async function deployBinary(
binPath: string,
from: Account,
ckb: CKB,
): Promise<{
deploymentRecipe: DeploymentRecipe;
deploymentOptions: DeploymentOptions;
}> {
const bin = await readFileToUint8Array(binPath);
const contractName = path.basename(binPath);
const result = await commons.deploy.generateDeployWithTypeIdTx({
cellProvider: ckb.indexer,
fromInfo: from.address,
scriptBinary: bin,
config: ckb.getLumosConfig(),
});

// send deploy tx
let txSkeleton = result.txSkeleton;
txSkeleton = commons.common.prepareSigningEntries(txSkeleton);
const message = txSkeleton.get('signingEntries').get(0)!.message;
const Sig = hd.key.signRecoverable(message!, from.privKey);
const tx = helpers.sealTransaction(txSkeleton, [Sig]);
const res = await ckb.rpc.sendTransaction(tx, 'passthrough');
console.log(`contract ${contractName} deployed, tx hash:`, res);
console.log('wait 4 blocks..');
await ckb.indexer.waitForSync(-4); // why negative 4? a bug in ckb-lumos

// todo: handle multiple cell recipes?
return {
deploymentOptions: {
name: contractName,
binFilePath: binPath,
enableTypeId: true,
lockScript: tx.outputs[+result.scriptConfig.INDEX].lock,
},
deploymentRecipe: {
cellRecipes: [
{
name: contractName,
txHash: result.scriptConfig.TX_HASH,
index: result.scriptConfig.INDEX,
occupiedCapacity: '0x' + BigInt(tx.outputsData[+result.scriptConfig.INDEX].slice(2).length / 2).toString(16),
dataHash: ckbHash(tx.outputsData[+result.scriptConfig.INDEX]),
typeId: computeScriptHash(result.typeId),
},
],
depGroupRecipes: [],
},
};
}
130 changes: 130 additions & 0 deletions src/deploy/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { DeploymentOptions, generateDeploymentToml } from '../deploy/toml';
import { DeploymentRecipe, generateDeploymentMigrationFile, Migration } from '../deploy/migration';
import { ckbHash, computeScriptHash } from '@ckb-lumos/lumos/utils';
import { genMyScriptsJsonFile } from '../scripts/gen';
import { OffCKBConfigFile } from '../template/offckb-config';
import { listBinaryFilesInFolder, readFileToUint8Array, isAbsolutePath } from '../util/fs';
import path from 'path';
import fs from 'fs';
import { HexString } from '@ckb-lumos/lumos';
import { Network } from '../util/type';
import { CKB } from '../sdk/ckb';

export type DeployBinaryReturnType = ReturnType<typeof deployBinary>;
export type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
export type DeployedInterfaceType = UnwrapPromise<DeployBinaryReturnType>;

export function getToDeployBinsPath() {
const userOffCKBConfigPath = path.resolve(process.cwd(), 'offckb.config.ts');
const fileContent = fs.readFileSync(userOffCKBConfigPath, 'utf-8');
const match = fileContent.match(/contractBinFolder:\s*['"]([^'"]+)['"]/);
if (match && match[1]) {
const contractBinFolderValue = match[1];
const binFolderPath = isAbsolutePath(contractBinFolderValue)
? contractBinFolderValue
: path.resolve(process.cwd(), contractBinFolderValue);
const bins = listBinaryFilesInFolder(binFolderPath);
return bins.map((bin) => path.resolve(binFolderPath, bin));
} else {
console.log('contractBinFolder value not found in offckb.config.ts');
return [];
}
}

export async function recordDeployResult(
results: DeployedInterfaceType[],
network: Network,
isUpdateMyScriptsJsonFile = true,
) {
if (results.length === 0) {
return;
}
for (const result of results) {
generateDeploymentToml(result.deploymentOptions, network);
generateDeploymentMigrationFile(result.deploymentOptions.name, result.deploymentRecipe, network);
}

// update my-scripts.json
if (isUpdateMyScriptsJsonFile) {
const userOffCKBConfigPath = path.resolve(process.cwd(), 'offckb.config.ts');
const folder = OffCKBConfigFile.readContractInfoFolder(userOffCKBConfigPath);
if (folder) {
const myScriptsFilePath = path.resolve(folder, 'my-scripts.json');
genMyScriptsJsonFile(myScriptsFilePath);
}
}

console.log('done.');
}

export async function deployBinaries(binPaths: string[], privateKey: HexString, enableTypeId: boolean, ckb: CKB) {
if (binPaths.length === 0) {
console.log('No binary to deploy.');
}
const results: DeployedInterfaceType[] = [];
for (const bin of binPaths) {
const result = await deployBinary(bin, privateKey, enableTypeId, ckb);
results.push(result);
}
return results;
}

export async function deployBinary(
binPath: string,
privateKey: HexString,
enableTypeId: boolean,
ckb: CKB,
): Promise<{
deploymentRecipe: DeploymentRecipe;
deploymentOptions: DeploymentOptions;
}> {
const bin = await readFileToUint8Array(binPath);
const contractName = path.basename(binPath);

const result = !enableTypeId
? await ckb.deployScript(bin, privateKey)
: Migration.isDeployedWithTypeId(contractName, ckb.network)
? await ckb.upgradeTypeIdScript(contractName, bin, privateKey)
: await ckb.deployNewTypeIDScript(bin, privateKey);

console.log(`contract ${contractName} deployed, tx hash:`, result.txHash);
console.log('wait for tx confirmed on-chain...');
await ckb.waitForTxConfirm(result.txHash);
console.log('tx committed.');

const txHash = result.txHash;
const typeIdScript = result.typeId;
const index = result.scriptOutputCellIndex;
const tx = result.tx;
const dataByteLen = BigInt(tx.outputsData[+index].slice(2).length / 2);
const dataShannonLen = dataByteLen * BigInt('100000000');
const occupiedCapacity = '0x' + dataShannonLen.toString(16);

if (enableTypeId && typeIdScript == null) {
throw new Error('type id script is null while enableTypeId is true.');
}
const typeIdScriptHash = enableTypeId ? computeScriptHash(typeIdScript!) : undefined;

// todo: handle multiple cell recipes?
return {
deploymentOptions: {
name: contractName,
binFilePath: binPath,
enableTypeId: enableTypeId,
lockScript: tx.outputs[+index].lock,
},
deploymentRecipe: {
cellRecipes: [
{
name: contractName,
txHash,
index: '0x' + index.toString(16),
occupiedCapacity,
dataHash: ckbHash(tx.outputsData[+index]),
typeId: typeIdScriptHash,
},
],
depGroupRecipes: [],
},
};
}
Loading

0 comments on commit 72c1322

Please sign in to comment.