From 37e6a05ba4510c9447eae68a2a45a8b03d364ead Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sat, 5 Aug 2023 09:37:38 -0500 Subject: [PATCH 1/3] NFT updates (#95) * through dl_owned_singletons * DL methods * nft_mint_bulk * nft updates * more nft stuffs --- src/CodeGen/Program.cs | 2 +- src/CodeGen/Properties/launchSettings.json | 2 +- src/chia-dotnet/ChiaTypes/Asset.cs | 22 +++ .../ChiaTypes/NFTBulkMintingInfo.cs | 41 +++++ src/chia-dotnet/ChiaTypes/NFTInfo.cs | 2 +- src/chia-dotnet/ChiaTypes/NFTMintingInfo.cs | 20 ++- src/chia-dotnet/ChiaTypes/NftCoinInfo.cs | 10 ++ src/chia-dotnet/Converters.cs | 14 ++ src/chia-dotnet/DIDWallet.cs | 2 +- src/chia-dotnet/DataLayerWallet.cs | 18 +-- src/chia-dotnet/NFTWallet.cs | 145 +++++++++++++++--- src/chia-dotnet/WalletProxy.cs | 78 +++++++++- 12 files changed, 307 insertions(+), 49 deletions(-) create mode 100644 src/chia-dotnet/ChiaTypes/Asset.cs create mode 100644 src/chia-dotnet/ChiaTypes/NFTBulkMintingInfo.cs create mode 100644 src/chia-dotnet/ChiaTypes/NftCoinInfo.cs diff --git a/src/CodeGen/Program.cs b/src/CodeGen/Program.cs index f9e5702f..0f01fd8e 100644 --- a/src/CodeGen/Program.cs +++ b/src/CodeGen/Program.cs @@ -153,7 +153,7 @@ private static string GetMethodBody(string dataInitialization, string? fullRetur return schema.Properties.Keys.FirstOrDefault(); } - throw new InvalidOperationException("Could not determine result key from schema"); + return null; } diff --git a/src/CodeGen/Properties/launchSettings.json b/src/CodeGen/Properties/launchSettings.json index 3c8c250b..e97ec0ca 100644 --- a/src/CodeGen/Properties/launchSettings.json +++ b/src/CodeGen/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "CodeGen": { "commandName": "Project", - "commandLineArgs": "C:\\Users\\don\\src\\github\\dkackman\\chia-api\\src\\wallet.yaml dl_singletons_by_root SingletonRecord" + "commandLineArgs": "C:\\Users\\don\\src\\github\\dkackman\\chia-api\\src\\wallet.yaml nft_calculate_royalties" } } } \ No newline at end of file diff --git a/src/chia-dotnet/ChiaTypes/Asset.cs b/src/chia-dotnet/ChiaTypes/Asset.cs new file mode 100644 index 00000000..0cb08e6a --- /dev/null +++ b/src/chia-dotnet/ChiaTypes/Asset.cs @@ -0,0 +1,22 @@ +namespace chia.dotnet +{ + public record AssetInfo + { + public string Asset { get; init; } = string.Empty; + public string Address { get; init; } = string.Empty; + public ulong Amount { get; init; } + } + + public record FungibleAsset + { + public string Asset { get; init; } = string.Empty; + public ulong Amount { get; init; } + } + + public record RoyaltyAsset + { + public string Asset { get; init; } = string.Empty; + public string RoyaltyAddress { get; init; } = string.Empty; + public ushort RoyaltyPercentage { get; init; } + } +} diff --git a/src/chia-dotnet/ChiaTypes/NFTBulkMintingInfo.cs b/src/chia-dotnet/ChiaTypes/NFTBulkMintingInfo.cs new file mode 100644 index 00000000..86a7a3cd --- /dev/null +++ b/src/chia-dotnet/ChiaTypes/NFTBulkMintingInfo.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace chia.dotnet +{ + /// + /// Info for minting NFTs in bulk + /// + public record NFTBulkMintingInfo + { + public string? RoyaltyAddress { get; init; } = string.Empty; + public ushort? RoyaltyPercentage { get; init; } + public IEnumerable MetadataList { get; init; } = new List(); + /// + /// a list of targets for transferring minted NFTs (aka airdrop) + /// + public IEnumerable? TargetList { get; init; } + /// + /// The starting point for mint number used in intermediate launcher puzzle + /// + public int MintNumberStart { get; init; } = 1; + /// + /// The total number of NFTs being minted + /// + public int? MintTotal { get; init; } + /// + /// For use with bulk minting to provide the coin used for funding the minting spend. + /// This coin can be one that will be created in the future + /// + public IEnumerable? XchCoins { get; init; } + /// + /// For use with bulk minting, so we can specify the puzzle hash that the change + // from the funding transaction goes to. + /// + public string? XchChangeTarget { get; init; } + public string? NewInnerpuzhash { get; init; } + public string? NewP2Puzhash { get; init; } + public Coin? DidCoin { get; init; } + public string? DidLineageParentHex { get; init; } + public bool MintFromDid { get; init; } + } +} diff --git a/src/chia-dotnet/ChiaTypes/NFTInfo.cs b/src/chia-dotnet/ChiaTypes/NFTInfo.cs index a190c185..0a9555c2 100644 --- a/src/chia-dotnet/ChiaTypes/NFTInfo.cs +++ b/src/chia-dotnet/ChiaTypes/NFTInfo.cs @@ -42,7 +42,7 @@ public record NFTInfo /// /// Hash of the metadata /// - public string MetaataHash { get; init; } = string.Empty; + public string MetadataHash { get; init; } = string.Empty; /// /// A list of license URIs /// diff --git a/src/chia-dotnet/ChiaTypes/NFTMintingInfo.cs b/src/chia-dotnet/ChiaTypes/NFTMintingInfo.cs index b67c35f0..56445cc4 100644 --- a/src/chia-dotnet/ChiaTypes/NFTMintingInfo.cs +++ b/src/chia-dotnet/ChiaTypes/NFTMintingInfo.cs @@ -2,15 +2,9 @@ namespace chia.dotnet { - /// - /// Info for minting an NFT - /// - public record NFTMintingInfo + public record NftMintEntry { - public string RoyaltyAddress { get; init; } = string.Empty; - public string TargetAddress { get; init; } = string.Empty; - public string? DIDID { get; init; } - public ushort RoyaltyPercentage { get; init; } = 0; + public ushort RoyaltyPercentage { get; init; } public IEnumerable Uris { get; init; } = new List(); public string Hash { get; init; } = string.Empty; public IEnumerable MetaUris { get; init; } = new List(); @@ -20,4 +14,14 @@ public record NFTMintingInfo public ulong EditionTotal { get; init; } = 1; public ulong EditionNumber { get; init; } = 1; } + + /// + /// Info for minting an NFT + /// + public record NFTMintingInfo : NftMintEntry + { + public string? RoyaltyAddress { get; init; } = string.Empty; + public string? TargetAddress { get; init; } + public string? DidId { get; init; } + } } diff --git a/src/chia-dotnet/ChiaTypes/NftCoinInfo.cs b/src/chia-dotnet/ChiaTypes/NftCoinInfo.cs new file mode 100644 index 00000000..448449c5 --- /dev/null +++ b/src/chia-dotnet/ChiaTypes/NftCoinInfo.cs @@ -0,0 +1,10 @@ +namespace chia.dotnet +{ + public record NftCoinInfo + { + + public string NftCoinId { get; init; } = string.Empty; + + public int WalletId { get; init; } + } +} diff --git a/src/chia-dotnet/Converters.cs b/src/chia-dotnet/Converters.cs index 2b93a1e1..1922cd58 100644 --- a/src/chia-dotnet/Converters.cs +++ b/src/chia-dotnet/Converters.cs @@ -40,6 +40,20 @@ internal static class Converters }); } + public static IDictionary ToDictionary(dynamic dictionary) + { + // TODO - this mneeds a unit test + // good data example here https://docs.chia.net/nft-rpc?_highlight=nft_calculate_royalties#nft_calculate_royalties + Debug.Assert(dictionary is not null); + // we're just gonna re-round trip this for now + var settings = new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() } + }; + var jsonString = JsonConvert.SerializeObject(dictionary, settings); + return JsonConvert.DeserializeObject>(jsonString, settings); + } + public static IEnumerable ToEnumerable(dynamic enumerable) { Debug.Assert(enumerable is not null); diff --git a/src/chia-dotnet/DIDWallet.cs b/src/chia-dotnet/DIDWallet.cs index 04bf6474..0c112d9e 100644 --- a/src/chia-dotnet/DIDWallet.cs +++ b/src/chia-dotnet/DIDWallet.cs @@ -94,7 +94,7 @@ public async Task Spend(string puzzlehash, CancellationToken cancellationToken = /// /// A token to allow the call to be cancelled /// A DID and optional CoinID - public async Task<(string MyDID, string? CoinID)> GetDID(CancellationToken cancellationToken = default) + public async Task<(string MyDid, string? CoinID)> GetDid(CancellationToken cancellationToken = default) { var response = await WalletProxy.SendMessage("did_get_did", CreateWalletDataObject(), cancellationToken).ConfigureAwait(false); diff --git a/src/chia-dotnet/DataLayerWallet.cs b/src/chia-dotnet/DataLayerWallet.cs index f3c58e82..6d6afe70 100644 --- a/src/chia-dotnet/DataLayerWallet.cs +++ b/src/chia-dotnet/DataLayerWallet.cs @@ -30,27 +30,11 @@ public override async Task Validate(CancellationToken cancellationToken = defaul await Validate(WalletType.DATA_LAYER, cancellationToken).ConfigureAwait(false); } - /// - /// Initialize the new data layer wallets. - /// - /// - /// - /// A token to allow the call to be cancelled - /// - public async Task<(IEnumerable Transactions, string LauncherId)> CreateNewDl(string root, ulong fee = 0, CancellationToken cancellationToken = default) - { - dynamic data = new ExpandoObject(); - data.root = root; - data.fee = fee; - var response = await WalletProxy.SendMessage("create_new_dl", data, cancellationToken).ConfigureAwait(false); - return (Converters.ToEnumerable(response.transactions), response.launcher_id); - } - /// /// Remove an existing mirror for a specific singleton. /// - /// /// + /// /// A token to allow the call to be cancelled /// A list of public async Task> DeleteMirror(string coinId, ulong fee = 0, CancellationToken cancellationToken = default) diff --git a/src/chia-dotnet/NFTWallet.cs b/src/chia-dotnet/NFTWallet.cs index 7db9a3f5..700ad714 100644 --- a/src/chia-dotnet/NFTWallet.cs +++ b/src/chia-dotnet/NFTWallet.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Dynamic; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -32,30 +34,53 @@ public override async Task Validate(CancellationToken cancellationToken = defaul /// Adds an Uri to an NFT /// /// The uri - /// The uri key + /// The type of uri: + /// * u Uri for the NFT data + /// * mu Uri for NFT metadata + /// * lu Uri for the NFT license /// The nft coin id + /// /// Transaction fee /// A token to allow the call to be cancelled /// An - public async Task AddUri(string uri, string key, string nftCoinId, ulong fee = 0, CancellationToken cancellationToken = default) + public async Task AddUri(string uri, string key, string nftCoinId, bool reusePuzhash = false, ulong fee = 0, CancellationToken cancellationToken = default) { dynamic data = CreateWalletDataObject(); data.uri = uri; data.key = key; data.nft_coin_id = nftCoinId; + data.reuse_puzhash = reusePuzhash; data.fee = fee; return await WalletProxy.SendMessage("nft_add_uri", "spend_bundle", data, cancellationToken).ConfigureAwait(false); } + /// + /// Retrieves the number of NFTs in a wallet. + /// + /// A token to allow the call to be cancelled + /// The number of NFTs in the wallet + public async Task NftCountNfts(CancellationToken cancellationToken = default) + { + return await WalletProxy.SendMessage("nft_count_nfts", CreateWalletDataObject(), "count", cancellationToken).ConfigureAwait(false); + } + /// /// Gets NFTs from a wallet /// + /// + /// + /// /// A token to allow the call to be cancelled - /// The DID id - public async Task> GetNFTs(CancellationToken cancellationToken = default) + /// A list of + public async Task> GetNFTs(int startIndex = 0, int num = 0, bool ignoreSizeLimit = false, CancellationToken cancellationToken = default) { - var response = await WalletProxy.SendMessage("nft_get_nfts", CreateWalletDataObject(), cancellationToken).ConfigureAwait(false); + dynamic data = CreateWalletDataObject(); + data.start_index = startIndex; + data.num = num; + data.ignore_size_limit = ignoreSizeLimit; + + var response = await WalletProxy.SendMessage("nft_get_nfts", data, cancellationToken).ConfigureAwait(false); return Converters.ToObject>(response, "nft_list"); } @@ -64,8 +89,8 @@ public async Task> GetNFTs(CancellationToken cancellationTo /// Gets the DID /// /// A token to allow the call to be cancelled - /// The list of NFTs - public async Task GetDID(CancellationToken cancellationToken = default) + /// The Did + public async Task GetDid(CancellationToken cancellationToken = default) { var response = await WalletProxy.SendMessage("nft_get_wallet_did", CreateWalletDataObject(), cancellationToken).ConfigureAwait(false); @@ -76,39 +101,119 @@ public async Task GetDID(CancellationToken cancellationToken = default) /// Mints an NFT /// /// Info about the NFT to be minted + /// /// Transaction fee /// A token to allow the call to be cancelled /// A - public async Task MintNFT(NFTMintingInfo info, ulong fee = 0, CancellationToken cancellationToken = default) + public async Task<(SpendBundle SpendBundle, string NftId)> MintNFT(NFTMintingInfo info, bool reusePuzhash = false, ulong fee = 0, CancellationToken cancellationToken = default) { dynamic data = CreateWalletDataObject(); - data.royalty_address = info.RoyaltyAddress; - data.target_address = info.TargetAddress; - data.uris = info.Uris; - data.meta_uris = info.MetaUris; - data.license_uris = info.LicenseUris; + if (info.RoyaltyAddress is not null) + { + data.royalty_address = info.RoyaltyAddress; + } + if (info.TargetAddress is not null) + { + data.target_address = info.TargetAddress; + } + if (info.MetaHash is not null) + { + data.meta_hash = info.MetaHash; + } + if (info.LicenseHash is not null) + { + data.license_hash = info.LicenseHash; + } + if (info.DidId is not null) + { + data.did_id = info.DidId; + } + data.uris = info.Uris.ToList(); + data.meta_uris = info.MetaUris.ToList(); + data.license_uris = info.LicenseUris.ToList(); data.hash = info.Hash; data.edition_number = info.EditionNumber; data.edition_total = info.EditionTotal; - data.meta_hash = info.MetaHash; - data.license_hash = info.LicenseHash; - data.did_id = info.DIDID; data.royalty_percentage = info.RoyaltyPercentage; data.fee = fee; + data.reuse_puzhash = reusePuzhash; - return await WalletProxy.SendMessage("nft_mint_nft", "spend_bundle", data, cancellationToken).ConfigureAwait(false); + var response = await WalletProxy.SendMessage("nft_mint_nft", data, cancellationToken).ConfigureAwait(false); + return (Converters.ToObject(response.spend_bundle), response.nft_id); + } + + /// + /// A list of dicts containing the metadata for each NFT to be minted + /// + /// A token to allow the call to be cancelled + /// Transaction fee + /// and a list of + public async Task<(SpendBundle SpendBundle, IEnumerable NftIdList)> NftMintBulk(NFTBulkMintingInfo info, bool reusePuzhash = false, ulong fee = 0, CancellationToken cancellationToken = default) + { + dynamic data = CreateWalletDataObject(); + + data.metadata_list = info.MetadataList.ToList(); + data.mint_number_start = info.MintNumberStart; + + if (info.RoyaltyAddress is not null) + { + data.royalty_address = info.RoyaltyAddress; + } + if (info.RoyaltyPercentage is not null) + { + data.royalty_percentage = info.RoyaltyPercentage; + } + if (info.TargetList is not null) + { + data.target_list = info.TargetList.ToList(); + } + if (info.MintTotal is not null) + { + data.mint_total = info.MintTotal; + } + if (info.XchCoins is not null) + { + data.xch_coin = info.XchCoins.ToList(); + } + if (info.XchChangeTarget is not null) + { + data.xch_change_target = info.XchChangeTarget; + } + if (info.NewInnerpuzhash is not null) + { + data.new_innerpuzhash = info.NewInnerpuzhash; + } + if (info.NewP2Puzhash is not null) + { + data.new_p2_puzhash = info.NewP2Puzhash; + } + if (info.DidCoin is not null) + { + data.did_coin = info.DidCoin; + } + if (info.DidLineageParentHex is not null) + { + data.did_lineage_parent_hex = info.DidLineageParentHex; + } + data.mint_from_did = info.MintFromDid; + data.fee = fee; + data.reuse_puzhash = reusePuzhash; + var response = await WalletProxy.SendMessage("nft_mint_bulk", data, cancellationToken).ConfigureAwait(false); + return (Converters.ToObject(response.spend_bundle), Converters.ToEnumerable(response.nft_id_list)); } /// /// Sets the DID for an NFT /// /// The DID ID + /// /// A token to allow the call to be cancelled /// A - public async Task SetDID(string didId, CancellationToken cancellationToken = default) + public async Task SetDID(string didId, bool reusePuzhash = false, CancellationToken cancellationToken = default) { dynamic data = CreateWalletDataObject(); data.did_id = didId; + data.reuse_puzhash = reusePuzhash; return await WalletProxy.SendMessage("nft_set_nft_did", "spend_bundle", data, cancellationToken).ConfigureAwait(false); } @@ -134,14 +239,16 @@ public async Task SetStatus(string coinId, bool inTransaction = true, Cancellati /// /// The target address /// The coin ID + /// /// Transaction fee /// A token to allow the call to be cancelled /// A - public async Task Transfer(string targetAddress, string coinId, ulong fee = 0, CancellationToken cancellationToken = default) + public async Task Transfer(string targetAddress, string coinId, bool reusePuzhash = false, ulong fee = 0, CancellationToken cancellationToken = default) { dynamic data = CreateWalletDataObject(); data.target_address = targetAddress; data.nft_coin_id = coinId; + data.reuse_puzhash = reusePuzhash; data.fee = fee; return await WalletProxy.SendMessage("nft_transfer_nft", "spend_bundle", data, cancellationToken).ConfigureAwait(false); diff --git a/src/chia-dotnet/WalletProxy.cs b/src/chia-dotnet/WalletProxy.cs index eb007831..63a357af 100644 --- a/src/chia-dotnet/WalletProxy.cs +++ b/src/chia-dotnet/WalletProxy.cs @@ -423,13 +423,17 @@ public async Task GetNFTByDID(string didId, CancellationToken cancellation /// /// The coin id /// Get latest NFT + /// + /// /// A token to allow the call to be cancelled /// The wallet id - public async Task GetNFTInfo(string coinId, bool latest = true, CancellationToken cancellationToken = default) + public async Task GetNFTInfo(string coinId, bool latest = true, bool ignoreSizeLimit = false, bool reusePuzhash = false, CancellationToken cancellationToken = default) { dynamic data = new ExpandoObject(); data.coin_id = coinId; data.latest = latest; + data.ignore_size_limit = ignoreSizeLimit; + data.reuse_puzhash = reusePuzhash; return await SendMessage("nft_get_info", data, "nft_info", cancellationToken).ConfigureAwait(false); } @@ -696,5 +700,77 @@ public async Task> GetCoinRecordsByNames(IEnumerable>("get_coin_records_by_names", data, "coin_records", cancellationToken).ConfigureAwait(false); } + + /// + /// Initialize the new data layer wallets. + /// + /// + /// + /// A token to allow the call to be cancelled + /// + public async Task<(IEnumerable Transactions, string LauncherId)> CreateNewDl(string root, ulong fee = 0, CancellationToken cancellationToken = default) + { + dynamic data = new ExpandoObject(); + data.root = root; + data.fee = fee; + var response = await SendMessage("create_new_dl", data, cancellationToken).ConfigureAwait(false); + return (Converters.ToEnumerable(response.transactions), response.launcher_id); + } + + /// + /// Transfers an NFT to another address. + /// + /// + /// + /// A token to allow the call to be cancelled + /// + public async Task>> CalculateRoyalties(IEnumerable fungibleAssets, IEnumerable royaltyAssets, CancellationToken cancellationToken = default) + { + dynamic data = new ExpandoObject(); + data.royalty_assets = royaltyAssets.ToList(); + data.fungible_assets = fungibleAssets.ToList(); + var response = await SendMessage("nft_calculate_royalties", data, cancellationToken).ConfigureAwait(false); + return Converters.ToDictionary>(response); + } + + /// + /// Bulk set DID for NFTs across different wallets. + /// + /// + /// + /// + /// + /// A token to allow the call to be cancelled + /// + public async Task<(int TxNum, SpendBundle SpendBundle)> NftSetDidBulk(string didId, IEnumerable nftCoinList, bool reusePuzhash = false, ulong fee = 0, CancellationToken cancellationToken = default) + { + dynamic data = new ExpandoObject(); + data.nft_coin_list = nftCoinList.ToList(); + data.did_id = didId; + data.fee = fee; + data.reuse_puzhash = reusePuzhash; + var response = await SendMessage("nft_set_did_bulk", data, cancellationToken).ConfigureAwait(false); + return (response.tx_num, Converters.ToObject(response.spend_bundle)); + } + + /// + /// Bulk transfer NFTs to an address. + /// + /// + /// + /// + /// + /// A token to allow the call to be cancelled + /// + public async Task<(int TxNum, SpendBundle SpendBundle)> NftTransferBulk(string targetAddress, IEnumerable nftCoinList, bool reusePuzhash = false, ulong fee = 0, CancellationToken cancellationToken = default) + { + dynamic data = new ExpandoObject(); + data.nft_coin_list = nftCoinList.ToList(); + data.target_address = targetAddress; + data.fee = fee; + data.reuse_puzhash = reusePuzhash; + var response = await SendMessage("nft_transfer_bulk", data, cancellationToken).ConfigureAwait(false); + return (response.tx_num, Converters.ToObject(response.spend_bundle)); + } } } From 03c668d103aef48b44249f0237c9184a7cec1ba0 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sat, 5 Aug 2023 09:42:52 -0500 Subject: [PATCH 2/3] Update dependabot.yml (#96) From f471edc941ce438c633535d6f3f1624495a94c8f Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sat, 5 Aug 2023 09:44:38 -0500 Subject: [PATCH 3/3] Create devskim.yml (#97) --- .github/workflows/devskim.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/devskim.yml diff --git a/.github/workflows/devskim.yml b/.github/workflows/devskim.yml new file mode 100644 index 00000000..74c53a09 --- /dev/null +++ b/.github/workflows/devskim.yml @@ -0,0 +1,34 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: DevSkim + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '25 16 * * 1' + +jobs: + lint: + name: DevSkim + runs-on: ubuntu-20.04 + permissions: + actions: read + contents: read + security-events: write + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Run DevSkim scanner + uses: microsoft/DevSkim-Action@v1 + + - name: Upload DevSkim scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: devskim-results.sarif