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: Add support for deploying arbitrary contracts #362

Merged
merged 8 commits into from
Jan 17, 2025
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Coinbase Node.js SDK Changelog

### Unreleased

- Add `deployContract` method to `WalletAddress` and `Wallet` to deploy an arbitrary contract.

## [0.14.0] - 2025-01-14

### Added
Expand Down
160 changes: 159 additions & 1 deletion src/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,68 @@ export interface BuildStakingOperationRequest {
*/
'options': { [key: string]: string; };
}
/**
*
* @export
* @interface CompileSmartContractRequest
*/
export interface CompileSmartContractRequest {
/**
* The JSON input containing the Solidity code, dependencies, and compiler settings.
* @type {string}
* @memberof CompileSmartContractRequest
*/
'solidity_input_json': string;
/**
* The name of the contract to compile.
* @type {string}
* @memberof CompileSmartContractRequest
*/
'contract_name': string;
/**
* The version of the Solidity compiler to use.
* @type {string}
* @memberof CompileSmartContractRequest
*/
'solidity_compiler_version': string;
}
/**
* Represents a compiled smart contract that can be deployed onchain
* @export
* @interface CompiledSmartContract
*/
export interface CompiledSmartContract {
/**
* The unique identifier of the compiled smart contract.
* @type {string}
* @memberof CompiledSmartContract
*/
'compiled_smart_contract_id'?: string;
/**
* The JSON-encoded input for the Solidity compiler
* @type {string}
* @memberof CompiledSmartContract
*/
'solidity_input_json'?: string;
/**
* The contract creation bytecode which will be used with constructor arguments to deploy the contract
* @type {string}
* @memberof CompiledSmartContract
*/
'contract_creation_bytecode'?: string;
/**
* The JSON-encoded ABI of the contract
* @type {string}
* @memberof CompiledSmartContract
*/
'abi'?: string;
/**
* The name of the smart contract to deploy
* @type {string}
* @memberof CompiledSmartContract
*/
'contract_name'?: string;
}
/**
* Represents a single decoded event emitted by a smart contract
* @export
Expand Down Expand Up @@ -835,6 +897,12 @@ export interface CreateSmartContractRequest {
* @memberof CreateSmartContractRequest
*/
'options': SmartContractOptions;
/**
* The optional UUID of the compiled smart contract to deploy. This field is only required when SmartContractType is set to custom.
* @type {string}
* @memberof CreateSmartContractRequest
*/
'compiled_smart_contract_id'?: string;
}


Expand Down Expand Up @@ -2766,6 +2834,12 @@ export interface SmartContract {
* @memberof SmartContract
*/
'is_external': boolean;
/**
* The ID of the compiled smart contract that was used to deploy this contract
* @type {string}
* @memberof SmartContract
*/
'compiled_smart_contract_id'?: string;
}


Expand Down Expand Up @@ -2914,7 +2988,7 @@ export interface SmartContractList {
* Options for smart contract creation
* @export
*/
export type SmartContractOptions = MultiTokenContractOptions | NFTContractOptions | TokenContractOptions;
export type SmartContractOptions = MultiTokenContractOptions | NFTContractOptions | TokenContractOptions | string;

/**
* The type of the smart contract.
Expand Down Expand Up @@ -8649,6 +8723,45 @@ export class ServerSignersApi extends BaseAPI implements ServerSignersApiInterfa
*/
export const SmartContractsApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
* Compile a smart contract
* @summary Compile a smart contract
* @param {CompileSmartContractRequest} compileSmartContractRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
compileSmartContract: async (compileSmartContractRequest: CompileSmartContractRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'compileSmartContractRequest' is not null or undefined
assertParamExists('compileSmartContract', 'compileSmartContractRequest', compileSmartContractRequest)
const localVarPath = `/v1/smart_contracts/compile`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}

const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;

// authentication apiKey required
await setApiKeyToObject(localVarHeaderParameter, "Jwt", configuration)



localVarHeaderParameter['Content-Type'] = 'application/json';

setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(compileSmartContractRequest, localVarRequestOptions, configuration)

return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* Create a new smart contract
* @summary Create a new smart contract
Expand Down Expand Up @@ -8992,6 +9105,19 @@ export const SmartContractsApiAxiosParamCreator = function (configuration?: Conf
export const SmartContractsApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = SmartContractsApiAxiosParamCreator(configuration)
return {
/**
* Compile a smart contract
* @summary Compile a smart contract
* @param {CompileSmartContractRequest} compileSmartContractRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async compileSmartContract(compileSmartContractRequest: CompileSmartContractRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CompiledSmartContract>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.compileSmartContract(compileSmartContractRequest, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['SmartContractsApi.compileSmartContract']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Create a new smart contract
* @summary Create a new smart contract
Expand Down Expand Up @@ -9106,6 +9232,16 @@ export const SmartContractsApiFp = function(configuration?: Configuration) {
export const SmartContractsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = SmartContractsApiFp(configuration)
return {
/**
* Compile a smart contract
* @summary Compile a smart contract
* @param {CompileSmartContractRequest} compileSmartContractRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
compileSmartContract(compileSmartContractRequest: CompileSmartContractRequest, options?: RawAxiosRequestConfig): AxiosPromise<CompiledSmartContract> {
return localVarFp.compileSmartContract(compileSmartContractRequest, options).then((request) => request(axios, basePath));
},
/**
* Create a new smart contract
* @summary Create a new smart contract
Expand Down Expand Up @@ -9198,6 +9334,16 @@ export const SmartContractsApiFactory = function (configuration?: Configuration,
* @interface SmartContractsApi
*/
export interface SmartContractsApiInterface {
/**
* Compile a smart contract
* @summary Compile a smart contract
* @param {CompileSmartContractRequest} compileSmartContractRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SmartContractsApiInterface
*/
compileSmartContract(compileSmartContractRequest: CompileSmartContractRequest, options?: RawAxiosRequestConfig): AxiosPromise<CompiledSmartContract>;

/**
* Create a new smart contract
* @summary Create a new smart contract
Expand Down Expand Up @@ -9290,6 +9436,18 @@ export interface SmartContractsApiInterface {
* @extends {BaseAPI}
*/
export class SmartContractsApi extends BaseAPI implements SmartContractsApiInterface {
/**
* Compile a smart contract
* @summary Compile a smart contract
* @param {CompileSmartContractRequest} compileSmartContractRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SmartContractsApi
*/
public compileSmartContract(compileSmartContractRequest: CompileSmartContractRequest, options?: RawAxiosRequestConfig) {
return SmartContractsApiFp(this.configuration).compileSmartContract(compileSmartContractRequest, options).then((request) => request(this.axios, this.basePath));
}

/**
* Create a new smart contract
* @summary Create a new smart contract
Expand Down
106 changes: 95 additions & 11 deletions src/coinbase/address/wallet_address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
PaginationResponse,
CreateFundOptions,
CreateQuoteOptions,
CreateCustomContractOptions,
} from "../types";
import { delay } from "../utils";
import { Wallet as WalletClass } from "../wallet";
Expand Down Expand Up @@ -380,7 +381,11 @@ export class WalletAddress extends Address {
* @returns A Promise that resolves to the deployed SmartContract object.
* @throws {APIError} If the API request to create a smart contract fails.
*/
public async deployToken({ name, symbol, totalSupply }: CreateERC20Options): Promise<SmartContract> {
public async deployToken({
name,
symbol,
totalSupply,
}: CreateERC20Options): Promise<SmartContract> {
if (!Coinbase.useServerSigner && !this.key) {
throw new Error("Cannot deploy ERC20 without private key loaded");
}
Expand Down Expand Up @@ -449,6 +454,44 @@ export class WalletAddress extends Address {
return smartContract;
}

/**
* Deploys a custom contract.
*
* @param options - The options for creating the custom contract.
* @param options.solidityVersion - The version of the solidity compiler, must be 0.8.+, such as "0.8.28+commit.7893614a". See https://binaries.soliditylang.org/bin/list.json
* @param options.solidityInputJson - The input json for the solidity compiler. See https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description for more details.
* @param options.contractName - The name of the contract class to be deployed.
* @param options.constructorArgs - The arguments for the constructor.
* @returns A Promise that resolves to the deployed SmartContract object.
* @throws {APIError} If the API request to create a smart contract fails.
*/
public async deployContract({
solidityVersion,
solidityInputJson,
contractName,
constructorArgs,
}: CreateCustomContractOptions): Promise<SmartContract> {
if (!Coinbase.useServerSigner && !this.key) {
throw new Error("Cannot deploy custom contract without private key loaded");
}

const smartContract = await this.createCustomContract({
solidityVersion,
solidityInputJson,
contractName,
constructorArgs,
});

if (Coinbase.useServerSigner) {
return smartContract;
}

await smartContract.sign(this.getSigner());
await smartContract.broadcast();

return smartContract;
}

/**
* Creates an ERC20 token contract.
*
Expand All @@ -460,7 +503,11 @@ export class WalletAddress extends Address {
* @returns {Promise<SmartContract>} A Promise that resolves to the created SmartContract.
* @throws {APIError} If the API request to create a smart contract fails.
*/
private async createERC20({ name, symbol, totalSupply }: CreateERC20Options): Promise<SmartContract> {
private async createERC20({
name,
symbol,
totalSupply,
}: CreateERC20Options): Promise<SmartContract> {
const resp = await Coinbase.apiClients.smartContract!.createSmartContract(
this.getWalletId(),
this.getId(),
Expand All @@ -486,7 +533,11 @@ export class WalletAddress extends Address {
* @returns A Promise that resolves to the deployed SmartContract object.
* @throws {APIError} If the private key is not loaded when not using server signer.
*/
private async createERC721({ name, symbol, baseURI }: CreateERC721Options): Promise<SmartContract> {
private async createERC721({
name,
symbol,
baseURI,
}: CreateERC721Options): Promise<SmartContract> {
const resp = await Coinbase.apiClients.smartContract!.createSmartContract(
this.getWalletId(),
this.getId(),
Expand Down Expand Up @@ -525,6 +576,45 @@ export class WalletAddress extends Address {
return SmartContract.fromModel(resp?.data);
}

/**
* Creates a custom contract.
*
* @private
* @param {CreateCustomContractOptions} options - The options for creating the custom contract.
* @param {string} options.solidityVersion - The version of the solidity compiler, must be 0.8.+, such as "0.8.28+commit.7893614a". See https://binaries.soliditylang.org/bin/list.json
* @param {string} options.solidityInputJson - The input json for the solidity compiler. See https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description for more details.
* @param {string} options.contractName - The name of the contract class.
* @param {Record<string, any>} options.constructorArgs - The arguments for the constructor.
* @returns {Promise<SmartContract>} A Promise that resolves to the created SmartContract.
* @throws {APIError} If the API request to compile or subsequently create a smart contract fails.
*/
private async createCustomContract({
solidityVersion,
solidityInputJson,
contractName,
constructorArgs,
}: CreateCustomContractOptions): Promise<SmartContract> {
const compileContractResp = await Coinbase.apiClients.smartContract!.compileSmartContract({
solidity_compiler_version: solidityVersion,
solidity_input_json: solidityInputJson,
contract_name: contractName,
});

const compiledContract = compileContractResp.data;
const compiledContractId = compiledContract.compiled_smart_contract_id;

const createContractResp = await Coinbase.apiClients.smartContract!.createSmartContract(
this.getWalletId(),
this.getId(),
{
type: SmartContractType.Custom,
options: JSON.stringify(constructorArgs),
compiled_smart_contract_id: compiledContractId,
},
);
return SmartContract.fromModel(createContractResp?.data);
}

/**
* Creates a contract invocation with the given data.
*
Expand Down Expand Up @@ -777,10 +867,7 @@ export class WalletAddress extends Address {
* @param options.assetId - The ID of the Asset to fund with. For Ether, eth, gwei, and wei are supported.
* @returns The created fund operation object
*/
public async fund({
amount,
assetId,
}: CreateFundOptions): Promise<FundOperation> {
public async fund({ amount, assetId }: CreateFundOptions): Promise<FundOperation> {
const normalizedAmount = new Decimal(amount.toString());

return FundOperation.create(
Expand All @@ -800,10 +887,7 @@ export class WalletAddress extends Address {
* @param options.assetId - The ID of the Asset to fund with. For Ether, eth, gwei, and wei are supported.
* @returns The fund quote object
*/
public async quoteFund({
amount,
assetId,
}: CreateQuoteOptions): Promise<FundQuote> {
public async quoteFund({ amount, assetId }: CreateQuoteOptions): Promise<FundQuote> {
const normalizedAmount = new Decimal(amount.toString());

return FundQuote.create(
Expand Down
Loading
Loading