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: merge v0.3.0-rc4 #155

Merged
merged 3 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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 @@ -210,6 +210,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
Loading