Skip to content

Commit

Permalink
AVM beta: deploy protocol, reactor contracts and basic call
Browse files Browse the repository at this point in the history
  • Loading branch information
wewewe authored and aaa committed Aug 13, 2024
1 parent 2506c56 commit a7bc9d6
Show file tree
Hide file tree
Showing 32 changed files with 1,263 additions and 106 deletions.
9 changes: 6 additions & 3 deletions .env
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
ELECTRUMX_PROXY_BASE_URL=
# Bitcoin:
# ELECTRUMX_PROXY_BASE_URL=https://epproxy.your-atomicals-electrumx-indexer/proxy
#
ELECTRUMX_PROXY_BASE_URL=
WALLET_PATH=./wallets
WALLET_FILE=wallet.tokens.json
WALLET_FILE=wallet.json
# testnet or livenet or regtest
NETWORK=livenet

CONCURRENCY=4
6 changes: 6 additions & 0 deletions lib/api/electrum-api-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ export class ElectrumApiMock implements ElectrumApiInterface {
public async atomicalsGetByTicker(ticker: string): Promise<any> {
return "atomicalsGetByTicker"
}
public async atomicalsGetByProtocol(name: string): Promise<any> {
return "atomicalsGetByProtocol"
}
public async atomicalsGetByContract(name: string): Promise<any> {
return "atomicalsGetByContract"
}
public async atomicalsGetByContainer(container: string): Promise<any> {
return "atomicalsGetByContainer"
}
Expand Down
2 changes: 2 additions & 0 deletions lib/api/electrum-api.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export interface ElectrumApiInterface {
atomicalsGetRealmInfo: (realmOrSubRealm: string) => Promise<any>;
atomicalsGetByTicker: (ticker: string) => Promise<any>;
atomicalsGetByContainer: (container: string) => Promise<any>;
atomicalsGetByProtocol: (name: string) => Promise<any>;
atomicalsGetByContract: (name: string) => Promise<any>;
atomicalsGetContainerItems: (container: string, limit: number, offset: number) => Promise<any>;
atomicalsFindTickers: (tickerPrefix: string | null, asc?: boolean) => Promise<any>;
atomicalsFindContainers: (containerPrefix: string | null, asc?: boolean) => Promise<any>;
Expand Down
10 changes: 9 additions & 1 deletion lib/api/electrum-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export class ElectrumApi implements ElectrumApiInterface {
return this.call('server.version', []);
}

public broadcast(rawtx: string, force = false): Promise<any> {
public broadcast(rawtx: string, force = true): Promise<any> {
return this.call(
force
? 'blockchain.transaction.broadcast_force'
Expand Down Expand Up @@ -265,6 +265,14 @@ export class ElectrumApi implements ElectrumApiInterface {
return this.call('blockchain.atomicals.get_by_ticker', [ticker]);
}

public atomicalsGetByProtocol(name: string): Promise<any> {
return this.call('blockchain.atomicals.get_by_protocol', [name]);
}

public atomicalsGetByContract(name: string): Promise<any> {
return this.call('blockchain.atomicals.get_by_contract', [name]);
}

public atomicalsGetByContainer(container: string): Promise<any> {
return this.call('blockchain.atomicals.get_by_container', [container]);
}
Expand Down
122 changes: 112 additions & 10 deletions lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ function getRandomBitwork(num: number) {

function groupAtomicalsUtxosByAtomicalId(atomical_utxos: any[]) {
const sorted = {};
// console.log('atomical_utxos', JSON.stringify(atomical_utxos, null, 2));
for (const utxo of atomical_utxos) {

for (const atomicalId of utxo['atomicals']) {
Expand All @@ -61,7 +60,6 @@ function groupAtomicalsUtxosByAtomicalId(atomical_utxos: any[]) {
return sorted
}
function showWalletFTBalancesDetails(obj: any, showutxos = false, accumulated) {
const atomicalsUtxosByAtomicalId = groupAtomicalsUtxosByAtomicalId(obj.atomicals_utxos);
for (const atomicalId in obj.atomicals_balances) {
if (!obj.atomicals_balances.hasOwnProperty(atomicalId)) {
continue;
Expand All @@ -79,13 +77,12 @@ function showWalletFTBalancesDetails(obj: any, showutxos = false, accumulated) {
console.log('Requested ticker status:', atomical['request_ticker_status']['status'])
console.log('Ticker:', atomical['ticker'])
console.log('Confirmed balance:', atomical['confirmed'])
console.log('UTXOs for Atomical:', atomicalsUtxosByAtomicalId[atomicalId].length);

// console.log('UTXOs for Atomical:', obj.atomicals_utxos);
accumulated[atomical['ticker']] = accumulated[atomical['ticker']] || 0;
accumulated[atomical['ticker']] += atomical['confirmed']
if (showutxos)
console.log(JSON.stringify(atomicalsUtxosByAtomicalId[atomicalId], null, 2));
}
console.log(JSON.stringify(obj.atomicals_balances, null, 2));
}
return accumulated
}

Expand Down Expand Up @@ -1755,7 +1752,6 @@ program.command('mint-nft')
}
});


program.command('mint-nft-json')
.description('Mint non-fungible token (NFT) Atomical with JSON only data')
.argument('<jsonFile>', 'string')
Expand Down Expand Up @@ -1797,7 +1793,114 @@ program.command('mint-nft-json')
console.log(error);
}
});

program.command('define-protocol')
.description('Define protocol')
.argument('<protocolName>', 'string')
.argument('<definitionfile>', 'string')
.option('--initialowner <string>', 'Initial owner wallet alias to mint the Atomical into')
.option('--satsbyte <number>', 'Satoshis per byte in fees', '-1')
.option('--satsoutput <number>', 'Satoshis to put into the minted atomical', '1000')
.option('--funding <string>', 'Use wallet alias WIF key to be used for funding and change')
.option('--bitworkc <string>', 'Whether to put any bitwork proof of work into the token mint. Applies to the commit transaction.')
.option('--bitworkr <string>', 'Whether to put any bitwork proof of work into the token mint. Applies to the reveal transaction.')
.action(async (protocolName, definitionfile, options) => {
try {
const walletInfo = await validateWalletStorage();
const config: ConfigurationInterface = validateCliInputs();
const atomicals = new Atomicals(ElectrumApi.createClient(process.env.ELECTRUMX_PROXY_BASE_URL || ''));
let initialOwnerAddress = resolveAddress(walletInfo, options.initialowner, walletInfo.primary).address;
let fundingRecord = resolveWalletAliasNew(walletInfo, options.funding, walletInfo.funding);
const result: any = await atomicals.mintProtocolInteractive({
meta: options.meta,
ctx: options.ctx,
init: options.init,
satsbyte: parseInt(options.satsbyte),
satsoutput: parseInt(options.satsoutput),
bitworkc: options.bitworkc,
bitworkr: options.bitworkr,
}, protocolName, definitionfile, initialOwnerAddress, fundingRecord.WIF);
handleResultLogging(result);
} catch (error) {
console.log(error);
}
});

program.command('deploy-contract')
.description('Deploy contract of a protocol')
.argument('<contractName>', 'string')
.argument('<protocolName>', 'string')
.option('--initialowner <string>', 'Initial owner wallet alias to mint the Atomical into')
.option('--satsbyte <number>', 'Satoshis per byte in fees', '-1')
.option('--satsoutput <number>', 'Satoshis to put into the minted atomical', '1000')
.option('--funding <string>', 'Use wallet alias WIF key to be used for funding and change')
.option('--bitworkc <string>', 'Whether to put any bitwork proof of work into the token mint. Applies to the commit transaction.')
.option('--bitworkr <string>', 'Whether to put any bitwork proof of work into the token mint. Applies to the reveal transaction.')
.action(async (contractName, protocolName, options) => {
try {
const walletInfo = await validateWalletStorage();
const config: ConfigurationInterface = validateCliInputs();
const atomicals = new Atomicals(ElectrumApi.createClient(process.env.ELECTRUMX_PROXY_BASE_URL || ''));
let initialOwnerAddress = resolveAddress(walletInfo, options.initialowner, walletInfo.primary).address;
let fundingRecord = resolveWalletAliasNew(walletInfo, options.funding, walletInfo.funding);
const result: any = await atomicals.mintContractInteractive({
meta: options.meta,
ctx: options.ctx,
init: options.init,
satsbyte: parseInt(options.satsbyte),
satsoutput: parseInt(options.satsoutput),
bitworkc: options.bitworkc,
bitworkr: options.bitworkr,
}, contractName, protocolName, null, initialOwnerAddress, fundingRecord.WIF);
handleResultLogging(result);
} catch (error) {
console.log(error);
}
});
/*
program.command('call-contract-testing')
.description('Call contract method')
.argument('<callFile>', 'number')
.option('--satsbyte <number>', 'Satoshis per byte in fees', '-1')
.option('--auth <string>', 'Use wallet alias WIF key to be used for identity. Uses primary by default')
.option('--funding <string>', 'Use wallet alias WIF key to be used for funding and change')
.action(async (callFile, options) => {
try {
const walletInfo = await validateWalletStorage();
const config: ConfigurationInterface = validateCliInputs();
const atomicals = new Atomicals(ElectrumApi.createClient(process.env.ELECTRUMX_PROXY_BASE_URL || ''));
let authRecord = resolveWalletAliasNew(walletInfo, options.auth, walletInfo.primary);
let fundingRecord = resolveWalletAliasNew(walletInfo, options.funding, walletInfo.funding);
const result: any = await atomicals.callContractInteractive({
satsbyte: parseInt(options.satsbyte),
}, callFile, authRecord, fundingRecord);
handleResultLogging(result);
} catch (error) {
console.log(error);
}
});*/

program.command('call-contract')
.description('Call contract method')
.argument('<callFile>', 'number')
.option('--satsbyte <number>', 'Satoshis per byte in fees', '-1')
.option('--auth <string>', 'Use wallet alias WIF key to be used for identity. Uses primary by default')
.option('--funding <string>', 'Use wallet alias WIF key to be used for funding and change')
.action(async (callFile, options) => {
try {
const walletInfo = await validateWalletStorage();
const config: ConfigurationInterface = validateCliInputs();
const atomicals = new Atomicals(ElectrumApi.createClient(process.env.ELECTRUMX_PROXY_BASE_URL || ''));
let authRecord = resolveWalletAliasNew(walletInfo, options.auth, walletInfo.primary);
let fundingRecord = resolveWalletAliasNew(walletInfo, options.funding, walletInfo.funding);
const result: any = await atomicals.callContractInteractive({
satsbyte: parseInt(options.satsbyte),
}, callFile, authRecord, fundingRecord);
handleResultLogging(result);
} catch (error) {
console.log(error);
}
});

program.command('mint-realm')
.description('Mint top level Realm non-fungible token (NFT) Atomical')
Expand Down Expand Up @@ -1977,9 +2080,8 @@ program.command('transfer-ft')
console.log(error);
}
});


program.command('transfer-builder')

program.command('transfer-builder')
.description('Transfer plain regular UTXOs to another addresses')
.option('--rbf', 'Whether to enable RBF for transactions.')
.option('--owner <string>', 'Use wallet alias WIF key to move the Atomical')
Expand Down
Empty file.
151 changes: 151 additions & 0 deletions lib/commands/call-interactive-contract-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { ElectrumApiInterface } from "../api/electrum-api.interface";
import { AtomicalsGetFetchType, CommandInterface } from "./command.interface";
import * as ecc from 'tiny-secp256k1';
import { ECPairFactory, ECPairAPI, TinySecp256k1Interface } from 'ecpair';
const bitcoin = require('bitcoinjs-lib');
const bitcoinMessage = require('bitcoinjs-message')
bitcoin.initEccLib(ecc);
import {
initEccLib,
} from "bitcoinjs-lib";
import { BaseRequestOptions } from "../interfaces/api.interface";
import { checkBaseRequestOptions } from "../utils/atomical-format-helpers";
import { jsonFileReader } from "../utils/file-utils";
import { AtomicalOperationBuilder } from "../utils/atomical-operation-builder";
import { GetByProtocolCommand } from "./get-by-protocol-command";
import { NETWORK, readFileAsCompleteDataObject, readJsonFileAsCompleteDataObjectEncodeAtomicalIds, readJsonFileAsCompleteDataObjectEncodeFields, readJsonFileAsCompleteDataObjectEncodeHash } from "./command-helpers";
import { GetByContractCommand } from "./get-by-contract-command";
import { IWalletRecord } from "../utils/validate-wallet-storage";
import { witnessStackToScriptWitness } from "./witness_stack_to_script_witness";
import { getKeypairInfo, KeyPairInfo } from "../utils/address-keypair-path";
const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1');
initEccLib(tinysecp as any);
const ECPair: ECPairAPI = ECPairFactory(tinysecp);
const crypto = require("crypto");
const eccrypto = require("eccrypto");


export interface ICallDetails {
contractName: string;
methodNumber: number;
};

export class CallInteractiveContractCommand implements CommandInterface {
constructor(
private electrumApi: ElectrumApiInterface,
private options: BaseRequestOptions,
private callFile: string,
private auth: IWalletRecord,
private funding: IWalletRecord,
) {
this.options = checkBaseRequestOptions(this.options)
}

validateCallDataFormat(callData) {
if (!callData['u'] || !callData['auth'] || !callData['n']) {
return false;
}
return true;
}

async run(): Promise<any> {
let callData = await readJsonFileAsCompleteDataObjectEncodeAtomicalIds(this.callFile, false);
callData['auth'] = this.auth.publicKey;
const success = this.validateCallDataFormat(callData);
if (!success) {
console.log('callData', callData);
throw new Error('Call data format requires fields n, u and auth')
}
const contractName = callData['n'];
callData['u'] = Buffer.from(callData['u'], 'hex');

console.log("====================================================================")
console.log("Call Contract Interactive")
console.log("====================================================================")

// Get the contract
const getContractCommand = new GetByContractCommand(this.electrumApi, contractName, AtomicalsGetFetchType.GET);
const getContractCommandResult = await getContractCommand.run();
if (!getContractCommandResult.success || !getContractCommandResult.data || !getContractCommandResult.data.result || !getContractCommandResult.data.result.atomical_id) {
throw 'Unknown contract name';
}
const protocolName = getContractCommandResult.data.result['$instance_of_protocol'];
const getProtocolCommand = new GetByProtocolCommand(this.electrumApi, protocolName, AtomicalsGetFetchType.GET);
const getProtocolCommandResult = await getProtocolCommand.run();
if (!getProtocolCommandResult.success || !getProtocolCommandResult.data || !getProtocolCommandResult.data.result || !getProtocolCommandResult.data.result.atomical_id) {
throw 'Unknown protocol instance name';
}
const atomicalBuilder = new AtomicalOperationBuilder({
electrumApi: this.electrumApi,
rbf: this.options.rbf,
satsbyte: this.options.satsbyte,
address: this.funding.address,
disableMiningChalk: this.options.disableMiningChalk,
opType: 'c',
nftOptions: {
satsoutput: this.options.satsoutput as any
},
meta: this.options.meta,
ctx: this.options.ctx,
init: this.options.init,
});
atomicalBuilder.setBitworkCommit(this.options.bitworkc || "a");
await atomicalBuilder.setData(callData);
const protocolLockCodeHex = getProtocolCommandResult.data.result['mint_data']['fields']['code']['$b'];
atomicalBuilder.setBeforeRevealSignCallback(async (commitUtxo: any, psbt: any) => {
callData['n'] = Buffer.from(callData['n'], 'utf8');
callData['u'] = Buffer.from(callData['u'], 'hex');
let outputVectorsHex = '';
for (const output of psbt.txOutputs) {
let numberValue: any = Buffer.allocUnsafe(8);
numberValue.writeBigUInt64LE(BigInt(output.value));
outputVectorsHex += numberValue.toString('hex');
outputVectorsHex += output.script.toString('hex');
}
const signature = await this.makeSig(commitUtxo.txid, commitUtxo.vout, callData['u'], protocolLockCodeHex, outputVectorsHex, this.auth);
let encodedSigPushData = this.encodePushDataToScript(signature);
psbt.addOutput({
script: encodedSigPushData,
value: 0
});
});
// The receiver output
atomicalBuilder.addOutput({
address: this.funding.address,
value: this.options.satsoutput as any || 1000
});
const result = await atomicalBuilder.start(this.funding.WIF);
return {
success: true,
data: result
}
}

async makeSig(txid: string, index: number, unlockScript, lockScript, outputVectorsHex, auth: IWalletRecord) {
let hexAuthMessage: any = (Buffer.from(txid, 'hex').reverse()).toString('hex');
let numberValue: any = Buffer.allocUnsafe(4);
numberValue.writeUint32LE(index);
hexAuthMessage += numberValue.toString('hex');
hexAuthMessage += unlockScript.toString('hex');
hexAuthMessage += lockScript;
hexAuthMessage += outputVectorsHex;
console.log('authMessage', hexAuthMessage)
const keyPair = ECPair.fromWIF(auth.WIF)
const privateKey = keyPair.privateKey;
const publicKey = eccrypto.getPublic(keyPair.privateKey);
const buf = Buffer.from(hexAuthMessage, 'hex')
// Always hash you message to sign!
const msg = crypto.createHash("sha256").update(buf).digest();
const signature = await eccrypto.sign(privateKey, msg).then(async (sig) => {
console.log("Signature in DER format (hex):", sig.toString('hex'))
return await eccrypto.verify(publicKey, msg, sig).then(function () {
return sig;
})
});
return signature;
}
encodePushDataToScript(sig) {
const hashscript = bitcoin.script.fromASM('OP_RETURN ' + Buffer.from('sig', 'utf8').toString('hex') + ' ' + sig.toString('hex'));
return hashscript;
}
}
Loading

0 comments on commit a7bc9d6

Please sign in to comment.