diff --git a/Makefile b/Makefile index 03f3f909..0ef63148 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ schema: @find contracts/* -maxdepth 2 -type f -name Cargo.toml -execdir cargo schema \; + @cd integration_tests && yarn build-ts-client test: @cargo test @@ -27,6 +28,7 @@ compile_arm64: --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ --platform linux/arm64 \ cosmwasm/workspace-optimizer-arm64:0.15.0 + @cd artifacts && for file in *-aarch64.wasm; do cp -f "$$file" "${file%-aarch64.wasm}.wasm"; done check_contracts: @cargo install cosmwasm-check diff --git a/contracts/factory/src/contract.rs b/contracts/factory/src/contract.rs index 64ae1d99..3eb4dec0 100644 --- a/contracts/factory/src/contract.rs +++ b/contracts/factory/src/contract.rs @@ -8,9 +8,11 @@ use cosmwasm_std::{ Deps, DepsMut, Env, HexBinary, MessageInfo, Response, StdResult, WasmMsg, }; use cw2::set_contract_version; -use lido_staking_base::msg::token::InstantiateMsg as TokenInstantiateMsg; + use lido_staking_base::{ helpers::answer::response, msg::core::InstantiateMsg as CoreInstantiateMsg, + msg::token::InstantiateMsg as TokenInstantiateMsg, + msg::voucher::InstantiateMsg as VoucherInstantiateMsg, }; use neutron_sdk::{ bindings::{msg::NeutronMsg, query::NeutronQuery}, @@ -35,6 +37,7 @@ pub fn instantiate( salt: msg.salt.to_string(), token_code_id: msg.token_code_id, core_code_id: msg.core_code_id, + voucher_code_id: msg.voucher_code_id, owner: info.sender.to_string(), subdenom: msg.subdenom.to_string(), }, @@ -44,6 +47,7 @@ pub fn instantiate( attr("salt", msg.salt), attr("token_code_id", msg.token_code_id.to_string()), attr("core_code_id", msg.core_code_id.to_string()), + attr("voucher_code_id", msg.voucher_code_id.to_string()), attr("owner", info.sender), attr("subdenom", msg.subdenom), ]; @@ -80,6 +84,7 @@ fn execute_init( let token_contract_checksum = get_code_checksum(deps.as_ref(), config.token_code_id)?; let core_contract_checksum = get_code_checksum(deps.as_ref(), config.core_code_id)?; + let voucher_contract_checksum = get_code_checksum(deps.as_ref(), config.voucher_code_id)?; let salt = config.salt.as_bytes(); let token_address = @@ -88,12 +93,17 @@ fn execute_init( let core_address = instantiate2_address(&core_contract_checksum, &canonical_self_address, salt)?; attrs.push(attr("core_address", core_address.to_string())); + let voucher_address = + instantiate2_address(&voucher_contract_checksum, &canonical_self_address, salt)?; + attrs.push(attr("voucher_address", voucher_address.to_string())); let core_contract = deps.api.addr_humanize(&core_address)?.to_string(); let token_contract = deps.api.addr_humanize(&token_address)?.to_string(); + let voucher_contract = deps.api.addr_humanize(&voucher_address)?.to_string(); let state = State { token_contract: token_contract.to_string(), core_contract: core_contract.to_string(), + voucher_contract: voucher_contract.to_string(), }; STATE.save(deps.storage, &state)?; @@ -101,7 +111,7 @@ fn execute_init( CosmosMsg::Wasm(WasmMsg::Instantiate2 { admin: Some(env.contract.address.to_string()), code_id: config.token_code_id, - label: "token".to_string(), + label: get_contract_label("token"), msg: to_json_binary(&TokenInstantiateMsg { core_address: core_contract, subdenom: config.subdenom, @@ -112,7 +122,7 @@ fn execute_init( CosmosMsg::Wasm(WasmMsg::Instantiate2 { admin: Some(env.contract.address.to_string()), code_id: config.core_code_id, - label: "core".to_string(), + label: get_contract_label("core"), msg: to_json_binary(&CoreInstantiateMsg { token_contract: token_contract.to_string(), puppeteer_contract: "".to_string(), @@ -122,6 +132,18 @@ fn execute_init( funds: vec![], salt: Binary::from(salt), }), + CosmosMsg::Wasm(WasmMsg::Instantiate2 { + admin: Some(env.contract.address.to_string()), + code_id: config.voucher_code_id, + label: get_contract_label("voucher"), + msg: to_json_binary(&VoucherInstantiateMsg { + name: "Lido Voucher".to_string(), + symbol: "LDOV".to_string(), + minter: core_address.to_string(), + })?, + funds: vec![], + salt: Binary::from(salt), + }), ]; Ok(response("execute-init", CONTRACT_NAME, attrs).add_messages(msgs)) @@ -131,3 +153,7 @@ fn get_code_checksum(deps: Deps, code_id: u64) -> NeutronResult { let CodeInfoResponse { checksum, .. } = deps.querier.query_wasm_code_info(code_id)?; Ok(checksum) } + +fn get_contract_label(base: &str) -> String { + format!("LIDO-staking-{}", base) +} diff --git a/contracts/factory/src/msg.rs b/contracts/factory/src/msg.rs index 5ecd6303..f271c0db 100644 --- a/contracts/factory/src/msg.rs +++ b/contracts/factory/src/msg.rs @@ -6,6 +6,7 @@ use crate::state::State; pub struct InstantiateMsg { pub token_code_id: u64, pub core_code_id: u64, + pub voucher_code_id: u64, pub salt: String, pub subdenom: String, } diff --git a/contracts/factory/src/state.rs b/contracts/factory/src/state.rs index a3498c97..c115f060 100644 --- a/contracts/factory/src/state.rs +++ b/contracts/factory/src/state.rs @@ -5,6 +5,7 @@ use cw_storage_plus::Item; pub struct Config { pub token_code_id: u64, pub core_code_id: u64, + pub voucher_code_id: u64, pub owner: String, pub salt: String, pub subdenom: String, @@ -14,6 +15,7 @@ pub struct Config { pub struct State { pub token_contract: String, pub core_contract: String, + pub voucher_contract: String, } pub const CONFIG: Item = Item::new("config"); diff --git a/integration_tests/src/generated/contractLib/index.ts b/integration_tests/src/generated/contractLib/index.ts index 2e9dd14d..32a96c8d 100644 --- a/integration_tests/src/generated/contractLib/index.ts +++ b/integration_tests/src/generated/contractLib/index.ts @@ -27,3 +27,6 @@ export const LidoValidatorsSet = _8; import * as _9 from './lidoValidatorsStats'; export const LidoValidatorsStats = _9; + +import * as _10 from './lidoWithdrawalVoucher'; +export const LidoWithdrawalVoucher = _10; diff --git a/integration_tests/src/generated/contractLib/lidoFactory.ts b/integration_tests/src/generated/contractLib/lidoFactory.ts index a3820053..f0de0249 100644 --- a/integration_tests/src/generated/contractLib/lidoFactory.ts +++ b/integration_tests/src/generated/contractLib/lidoFactory.ts @@ -6,6 +6,7 @@ export interface InstantiateMsg { salt: string; subdenom: string; token_code_id: number; + voucher_code_id: number; } export interface LidoFactorySchema { responses: State; @@ -14,6 +15,7 @@ export interface LidoFactorySchema { export interface State { core_contract: string; token_contract: string; + voucher_contract: string; } diff --git a/integration_tests/src/generated/contractLib/lidoWithdrawalVoucher.ts b/integration_tests/src/generated/contractLib/lidoWithdrawalVoucher.ts new file mode 100644 index 00000000..10a08899 --- /dev/null +++ b/integration_tests/src/generated/contractLib/lidoWithdrawalVoucher.ts @@ -0,0 +1,460 @@ +import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult, InstantiateResult } from "@cosmjs/cosmwasm-stargate"; +import { StdFee } from "@cosmjs/amino"; +import { Coin } from "@cosmjs/amino"; +export interface InstantiateMsg { + /** + * The minter is the only one who can create new NFTs. This is designed for a base NFT that is controlled by an external program or contract. You will likely replace this with custom logic in custom NFTs + */ + minter: string; + /** + * Name of the NFT contract + */ + name: string; + /** + * Symbol of the NFT contract + */ + symbol: string; +} +/** + * Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future) + */ +export type Expiration = + | { + at_height: number; + } + | { + at_time: Timestamp; + } + | { + never: {}; + }; +/** + * A point in time in nanosecond precision. + * + * This type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z. + * + * ## Examples + * + * ``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202); + * + * let ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ``` + */ +export type Timestamp = Uint64; +/** + * A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq. + * + * # Examples + * + * Use `from` to create instances of this and `u64` to get the value out: + * + * ``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42); + * + * let b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ``` + */ +export type Uint64 = string; +export type Null = null; +/** + * Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline. + * + * This is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also . + */ +export type Binary = string; +/** + * A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq. + * + * # Examples + * + * Use `from` to create instances of this and `u128` to get the value out: + * + * ``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123); + * + * let b = Uint128::from(42u64); assert_eq!(b.u128(), 42); + * + * let c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ``` + */ +export type Uint128 = string; + +export interface LidoWithdrawalVoucherSchema { + responses: + | AllNftInfoResponseFor_Empty + | OperatorsResponse + | TokensResponse + | ApprovalResponse + | ApprovalsResponse + | ContractInfoResponse + | Null + | MinterResponse + | NftInfoResponseFor_Empty1 + | NumTokensResponse + | OperatorResponse + | OwnerOfResponse1 + | OwnershipFor_String + | TokensResponse1; + query: + | OwnerOfArgs + | ApprovalArgs + | ApprovalsArgs + | OperatorArgs + | AllOperatorsArgs + | NftInfoArgs + | AllNftInfoArgs + | TokensArgs + | ExtensionArgs; + execute: + | TransferNftArgs + | SendNftArgs + | ApproveArgs + | RevokeArgs + | ApproveAllArgs + | RevokeAllArgs + | MintArgs + | BurnArgs + | ExtensionArgs1; + [k: string]: unknown; +} +export interface AllNftInfoResponseFor_Empty { + /** + * Who can transfer the token + */ + access: OwnerOfResponse; + /** + * Data on the token itself, + */ + info: NftInfoResponseFor_Empty; +} +export interface OwnerOfResponse { + /** + * If set this address is approved to transfer/send the token as well + */ + approvals: Approval[]; + /** + * Owner of the token + */ + owner: string; +} +export interface Approval { + /** + * When the Approval expires (maybe Expiration::never) + */ + expires: Expiration; + /** + * Account that can transfer/send the token + */ + spender: string; +} +export interface NftInfoResponseFor_Empty { + /** + * You can add any custom metadata here when you extend cw721-base + */ + extension: Empty; + /** + * Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema + */ + token_uri?: string | null; +} +/** + * An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message. + * + * It is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451) + */ +export interface Empty { + [k: string]: unknown; +} +export interface OperatorsResponse { + operators: Approval[]; +} +export interface TokensResponse { + /** + * Contains all token_ids in lexicographical ordering If there are more than `limit`, use `start_after` in future queries to achieve pagination. + */ + tokens: string[]; +} +export interface ApprovalResponse { + approval: Approval; +} +export interface ApprovalsResponse { + approvals: Approval[]; +} +export interface ContractInfoResponse { + name: string; + symbol: string; +} +/** + * Shows who can mint these tokens + */ +export interface MinterResponse { + minter?: string | null; +} +export interface NftInfoResponseFor_Empty1 { + /** + * You can add any custom metadata here when you extend cw721-base + */ + extension: Empty; + /** + * Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema + */ + token_uri?: string | null; +} +export interface NumTokensResponse { + count: number; +} +export interface OperatorResponse { + approval: Approval; +} +export interface OwnerOfResponse1 { + /** + * If set this address is approved to transfer/send the token as well + */ + approvals: Approval[]; + /** + * Owner of the token + */ + owner: string; +} +/** + * The contract's ownership info + */ +export interface OwnershipFor_String { + /** + * The contract's current owner. `None` if the ownership has been renounced. + */ + owner?: string | null; + /** + * The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline. + */ + pending_expiry?: Expiration | null; + /** + * The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer. + */ + pending_owner?: string | null; +} +export interface TokensResponse1 { + /** + * Contains all token_ids in lexicographical ordering If there are more than `limit`, use `start_after` in future queries to achieve pagination. + */ + tokens: string[]; +} +export interface OwnerOfArgs { + /** + * unset or false will filter out expired approvals, you must set to true to see them + */ + include_expired?: boolean | null; + token_id: string; +} +export interface ApprovalArgs { + include_expired?: boolean | null; + spender: string; + token_id: string; +} +export interface ApprovalsArgs { + include_expired?: boolean | null; + token_id: string; +} +export interface OperatorArgs { + include_expired?: boolean | null; + operator: string; + owner: string; +} +export interface AllOperatorsArgs { + /** + * unset or false will filter out expired items, you must set to true to see them + */ + include_expired?: boolean | null; + limit?: number | null; + owner: string; + start_after?: string | null; +} +export interface NftInfoArgs { + token_id: string; +} +export interface AllNftInfoArgs { + /** + * unset or false will filter out expired approvals, you must set to true to see them + */ + include_expired?: boolean | null; + token_id: string; +} +export interface TokensArgs { + limit?: number | null; + owner: string; + start_after?: string | null; +} +export interface ExtensionArgs { + msg: Empty; +} +export interface TransferNftArgs { + recipient: string; + token_id: string; +} +export interface SendNftArgs { + contract: string; + msg: Binary; + token_id: string; +} +export interface ApproveArgs { + expires?: Expiration | null; + spender: string; + token_id: string; +} +export interface RevokeArgs { + spender: string; + token_id: string; +} +export interface ApproveAllArgs { + expires?: Expiration | null; + operator: string; +} +export interface RevokeAllArgs { + operator: string; +} +export interface MintArgs { + /** + * Any custom extension used by this contract + */ + extension?: Metadata | null; + /** + * The owner of the newly minter NFT + */ + owner: string; + /** + * Unique ID of the NFT + */ + token_id: string; + /** + * Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema + */ + token_uri?: string | null; +} +export interface Metadata { + amount: Uint128; + attributes?: Trait[] | null; + batch_id: string; + description?: string | null; + name: string; +} +export interface Trait { + display_type?: string | null; + trait_type: string; + value: string; +} +export interface BurnArgs { + token_id: string; +} +export interface ExtensionArgs1 { + msg: Empty; +} + + +function isSigningCosmWasmClient( + client: CosmWasmClient | SigningCosmWasmClient +): client is SigningCosmWasmClient { + return 'execute' in client; +} + +export class Client { + private readonly client: CosmWasmClient | SigningCosmWasmClient; + contractAddress: string; + constructor(client: CosmWasmClient | SigningCosmWasmClient, contractAddress: string) { + this.client = client; + this.contractAddress = contractAddress; + } + mustBeSigningClient() { + return new Error("This client is not a SigningCosmWasmClient"); + } + static async instantiate( + client: SigningCosmWasmClient, + sender: string, + codeId: number, + initMsg: InstantiateMsg, + label: string, + initCoins?: readonly Coin[], + fees?: StdFee | 'auto' | number, + ): Promise { + const res = await client.instantiate(sender, codeId, initMsg, label, fees, { + ...(initCoins && initCoins.length && { funds: initCoins }), + }); + return res; + } + queryOwnerOf = async(args: OwnerOfArgs): Promise => { + return this.client.queryContractSmart(this.contractAddress, { owner_of: args }); + } + queryApproval = async(args: ApprovalArgs): Promise => { + return this.client.queryContractSmart(this.contractAddress, { approval: args }); + } + queryApprovals = async(args: ApprovalsArgs): Promise => { + return this.client.queryContractSmart(this.contractAddress, { approvals: args }); + } + queryOperator = async(args: OperatorArgs): Promise => { + return this.client.queryContractSmart(this.contractAddress, { operator: args }); + } + queryAllOperators = async(args: AllOperatorsArgs): Promise => { + return this.client.queryContractSmart(this.contractAddress, { all_operators: args }); + } + queryNumTokens = async(): Promise => { + return this.client.queryContractSmart(this.contractAddress, { num_tokens: {} }); + } + queryContractInfo = async(): Promise => { + return this.client.queryContractSmart(this.contractAddress, { contract_info: {} }); + } + queryNftInfo = async(args: NftInfoArgs): Promise => { + return this.client.queryContractSmart(this.contractAddress, { nft_info: args }); + } + queryAllNftInfo = async(args: AllNftInfoArgs): Promise => { + return this.client.queryContractSmart(this.contractAddress, { all_nft_info: args }); + } + queryTokens = async(args: TokensArgs): Promise => { + return this.client.queryContractSmart(this.contractAddress, { tokens: args }); + } + queryAllTokens = async(): Promise => { + return this.client.queryContractSmart(this.contractAddress, { all_tokens: {} }); + } + queryMinter = async(): Promise => { + return this.client.queryContractSmart(this.contractAddress, { minter: {} }); + } + queryExtension = async(args: ExtensionArgs): Promise => { + return this.client.queryContractSmart(this.contractAddress, { extension: args }); + } + queryOwnership = async(): Promise => { + return this.client.queryContractSmart(this.contractAddress, { ownership: {} }); + } + transferNft = async(sender:string, args: TransferNftArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { transfer_nft: args }, fee || "auto", memo, funds); + } + sendNft = async(sender:string, args: SendNftArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { send_nft: args }, fee || "auto", memo, funds); + } + approve = async(sender:string, args: ApproveArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { approve: args }, fee || "auto", memo, funds); + } + revoke = async(sender:string, args: RevokeArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { revoke: args }, fee || "auto", memo, funds); + } + approveAll = async(sender:string, args: ApproveAllArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { approve_all: args }, fee || "auto", memo, funds); + } + revokeAll = async(sender:string, args: RevokeAllArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { revoke_all: args }, fee || "auto", memo, funds); + } + mint = async(sender:string, args: MintArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { mint: args }, fee || "auto", memo, funds); + } + burn = async(sender:string, args: BurnArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { burn: args }, fee || "auto", memo, funds); + } + extension = async(sender:string, args: ExtensionArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { extension: args }, fee || "auto", memo, funds); + } + updateOwnership = async(sender: string, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { update_ownership: {} }, fee || "auto", memo, funds); + } +}