From cb38a3bb1d9c421127d9ee7adf5ba74640dde1b0 Mon Sep 17 00:00:00 2001 From: Sergey Timoshin Date: Fri, 31 Jan 2025 02:33:00 +0100 Subject: [PATCH] feat: memo example --- Cargo.lock | 16 + Cargo.toml | 1 + examples/memo/.gitignore | 8 + examples/memo/.prettierignore | 8 + examples/memo/Anchor.toml | 18 ++ examples/memo/README.md | 1 + examples/memo/migrations/deploy.ts | 12 + examples/memo/package.json | 22 ++ examples/memo/programs/memo/Cargo.toml | 37 +++ examples/memo/programs/memo/Xargo.toml | 2 + examples/memo/programs/memo/src/lib.rs | 163 ++++++++++ examples/memo/programs/memo/tests/test.rs | 377 ++++++++++++++++++++++ examples/memo/tsconfig.json | 10 + 13 files changed, 675 insertions(+) create mode 100644 examples/memo/.gitignore create mode 100644 examples/memo/.prettierignore create mode 100644 examples/memo/Anchor.toml create mode 100644 examples/memo/README.md create mode 100644 examples/memo/migrations/deploy.ts create mode 100644 examples/memo/package.json create mode 100644 examples/memo/programs/memo/Cargo.toml create mode 100644 examples/memo/programs/memo/Xargo.toml create mode 100644 examples/memo/programs/memo/src/lib.rs create mode 100644 examples/memo/programs/memo/tests/test.rs create mode 100644 examples/memo/tsconfig.json diff --git a/Cargo.lock b/Cargo.lock index 9458c63617..5368e455f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3412,6 +3412,22 @@ dependencies = [ "libc", ] +[[package]] +name = "memo" +version = "0.0.1" +dependencies = [ + "anchor-lang", + "borsh 0.10.3", + "light-client", + "light-hasher", + "light-program-test", + "light-sdk", + "light-test-utils", + "light-utils", + "solana-sdk", + "tokio", +] + [[package]] name = "memoffset" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index f582287096..d47c7f9740 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ members = [ "examples/token-escrow/programs/*", "examples/name-service/programs/name-service-without-macros", "examples/counter/programs/counter", + "examples/memo/programs/memo", "program-tests/account-compression-test/", "program-tests/compressed-token-test/", "program-tests/e2e-test/", diff --git a/examples/memo/.gitignore b/examples/memo/.gitignore new file mode 100644 index 0000000000..8d401163fb --- /dev/null +++ b/examples/memo/.gitignore @@ -0,0 +1,8 @@ + +.anchor +.DS_Store +target +**/*.rs.bk +node_modules +test-ledger +.yarn diff --git a/examples/memo/.prettierignore b/examples/memo/.prettierignore new file mode 100644 index 0000000000..c1a0b75f09 --- /dev/null +++ b/examples/memo/.prettierignore @@ -0,0 +1,8 @@ + +.anchor +.DS_Store +target +node_modules +dist +build +test-ledger diff --git a/examples/memo/Anchor.toml b/examples/memo/Anchor.toml new file mode 100644 index 0000000000..bac479830d --- /dev/null +++ b/examples/memo/Anchor.toml @@ -0,0 +1,18 @@ +[toolchain] + +[features] +seeds = false +skip-lint = false + +[programs.localnet] +counter = "GRLu2hKaAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPqX" + +[registry] +url = "https://api.apr.dev" + +[provider] +cluster = "Localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" diff --git a/examples/memo/README.md b/examples/memo/README.md new file mode 100644 index 0000000000..1239200201 --- /dev/null +++ b/examples/memo/README.md @@ -0,0 +1 @@ +# Counter program example diff --git a/examples/memo/migrations/deploy.ts b/examples/memo/migrations/deploy.ts new file mode 100644 index 0000000000..82fb175fa2 --- /dev/null +++ b/examples/memo/migrations/deploy.ts @@ -0,0 +1,12 @@ +// Migrations are an early feature. Currently, they're nothing more than this +// single deploy script that's invoked from the CLI, injecting a provider +// configured from the workspace's Anchor.toml. + +const anchor = require("@coral-xyz/anchor"); + +module.exports = async function (provider) { + // Configure client to use the provider. + anchor.setProvider(provider); + + // Add your deploy script here. +}; diff --git a/examples/memo/package.json b/examples/memo/package.json new file mode 100644 index 0000000000..d42abba1c7 --- /dev/null +++ b/examples/memo/package.json @@ -0,0 +1,22 @@ +{ + "scripts": { + "lint:fix": "prettier \"*/**/*{.js,.ts}\" -w", + "lint": "prettier \"*/**/*{.js,.ts}\" --check", + "test": "cargo test-sbf -p memo -- --test-threads 1" + }, + "dependencies": { + "@coral-xyz/anchor": "^0.29.0" + }, + "devDependencies": { + "@lightprotocol/zk-compression-cli": "workspace:*", + + "chai": "^5.1.2", + "mocha": "^11.0.1", + "ts-mocha": "^10.0.0", + "@types/bn.js": "^5.1.6", + "@types/chai": "^5.0.1", + "@types/mocha": "^10.0.7", + "typescript": "^5.7.2", + "prettier": "^3.4.2" + } +} diff --git a/examples/memo/programs/memo/Cargo.toml b/examples/memo/programs/memo/Cargo.toml new file mode 100644 index 0000000000..63da43c526 --- /dev/null +++ b/examples/memo/programs/memo/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "memo" +version = "0.0.1" +description = "Created with Anchor" +edition = "2021" +rust-version = "1.75.0" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib", "lib"] +name = "memo" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = ["idl-build"] +test-sbf = [] +bench-sbf = [] +idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] + +[dependencies] +anchor-lang = { workspace = true } +borsh = { workspace = true } +light-hasher = { workspace = true, features = ["solana"] } +light-sdk = { workspace = true } +light-utils = { workspace = true } + +[target.'cfg(not(target_os = "solana"))'.dependencies] +solana-sdk = { workspace = true } + +[dev-dependencies] +light-client = { workspace = true , features = ["devenv"]} +light-test-utils = { workspace = true, features = ["devenv"] } +light-program-test = { workspace = true, features = ["devenv"] } +tokio = "1.36.0" diff --git a/examples/memo/programs/memo/Xargo.toml b/examples/memo/programs/memo/Xargo.toml new file mode 100644 index 0000000000..475fb71ed1 --- /dev/null +++ b/examples/memo/programs/memo/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/examples/memo/programs/memo/src/lib.rs b/examples/memo/programs/memo/src/lib.rs new file mode 100644 index 0000000000..2d3f6fdd37 --- /dev/null +++ b/examples/memo/programs/memo/src/lib.rs @@ -0,0 +1,163 @@ +use anchor_lang::prelude::*; +use borsh::BorshDeserialize; +use light_sdk::{ + account::LightAccount, instruction_data::LightInstructionData, light_system_accounts, + verify::verify_light_accounts, LightDiscriminator, LightHasher, LightTraits, +}; + +declare_id!("GRLu2hKaAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPqX"); + +#[program] +pub mod memo { + use light_hasher::Discriminator; + use light_sdk::{ + address::derive_address, error::LightSdkError, + program_merkle_context::unpack_address_merkle_context, + }; + + use super::*; + + pub fn create_memo<'info>( + ctx: Context<'_, '_, '_, 'info, CreateMemo<'info>>, + inputs: Vec, + message: String, + ) -> Result<()> { + let inputs = LightInstructionData::deserialize(&inputs)?; + let accounts = inputs + .accounts + .as_ref() + .ok_or(LightSdkError::ExpectedAccounts)?; + + let address_merkle_context = accounts[0] + .address_merkle_context + .ok_or(LightSdkError::ExpectedAddressMerkleContext)?; + let address_merkle_context = + unpack_address_merkle_context(address_merkle_context, ctx.remaining_accounts); + let (address, address_seed) = derive_address( + &[b"memo", ctx.accounts.signer.key().as_ref()], + &address_merkle_context, + &crate::ID, + ); + + let mut memo: LightAccount<'_, MemoAccount> = LightAccount::from_meta_init( + &accounts[0], + MemoAccount::discriminator(), + address, + address_seed, + &crate::ID, + )?; + + memo.authority = ctx.accounts.signer.key(); + memo.message = message; + + verify_light_accounts(&ctx, inputs.proof, &[memo], None, false, None)?; + + Ok(()) + } + + pub fn update_memo<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateMemo<'info>>, + inputs: Vec, + new_message: String, + ) -> Result<()> { + let inputs = LightInstructionData::deserialize(&inputs)?; + let accounts = inputs + .accounts + .as_ref() + .ok_or(LightSdkError::ExpectedAccounts)?; + + let mut memo: LightAccount<'_, MemoAccount> = + LightAccount::from_meta_mut(&accounts[0], MemoAccount::discriminator(), &crate::ID)?; + + if memo.authority != ctx.accounts.signer.key() { + return err!(CustomError::Unauthorized); + } + + memo.message = new_message; + + verify_light_accounts(&ctx, inputs.proof, &[memo], None, false, None)?; + + Ok(()) + } + + pub fn delete_memo<'info>( + ctx: Context<'_, '_, '_, 'info, DeleteMemo<'info>>, + inputs: Vec, + ) -> Result<()> { + let inputs = LightInstructionData::deserialize(&inputs)?; + let accounts = inputs + .accounts + .as_ref() + .ok_or(LightSdkError::ExpectedAccounts)?; + + let memo: LightAccount<'_, MemoAccount> = + LightAccount::from_meta_close(&accounts[0], MemoAccount::discriminator(), &crate::ID)?; + + if memo.authority != ctx.accounts.signer.key() { + return err!(CustomError::Unauthorized); + } + + verify_light_accounts(&ctx, inputs.proof, &[memo], None, false, None)?; + + Ok(()) + } +} + +// Memo account structure +#[derive( + Clone, Debug, Default, AnchorDeserialize, AnchorSerialize, LightDiscriminator, LightHasher, +)] +pub struct MemoAccount { + #[truncate] + pub authority: Pubkey, + pub message: String, +} + +// Custom errors +#[error_code] +pub enum CustomError { + #[msg("You are not authorized to perform this action.")] + Unauthorized, +} + +// Context for creating a memo +#[light_system_accounts] +#[derive(Accounts, LightTraits)] +pub struct CreateMemo<'info> { + #[account(mut)] + #[fee_payer] + pub signer: Signer<'info>, + #[self_program] + pub self_program: Program<'info, crate::program::Memo>, + /// CHECK: Checked in light-system-program. + #[authority] + pub cpi_signer: AccountInfo<'info>, +} + +// Context for updating a memo +#[light_system_accounts] +#[derive(Accounts, LightTraits)] +pub struct UpdateMemo<'info> { + #[account(mut)] + #[fee_payer] + pub signer: Signer<'info>, + #[self_program] + pub self_program: Program<'info, crate::program::Memo>, + /// CHECK: Checked in light-system-program. + #[authority] + pub cpi_signer: AccountInfo<'info>, +} + +// Context for deleting a memo +#[light_system_accounts] +#[derive(Accounts, LightTraits)] +pub struct DeleteMemo<'info> { + #[account(mut)] + #[fee_payer] + pub signer: Signer<'info>, + #[self_program] + pub self_program: Program<'info, crate::program::Memo>, + /// CHECK: Checked in light-system-program. + #[authority] + pub cpi_signer: AccountInfo<'info>, +} diff --git a/examples/memo/programs/memo/tests/test.rs b/examples/memo/programs/memo/tests/test.rs new file mode 100644 index 0000000000..cd665f0659 --- /dev/null +++ b/examples/memo/programs/memo/tests/test.rs @@ -0,0 +1,377 @@ +#![cfg(feature = "test-sbf")] + +use anchor_lang::{AnchorDeserialize, InstructionData, ToAccountMetas}; +use light_client::{ + indexer::{AddressMerkleTreeAccounts, Indexer, StateMerkleTreeAccounts}, + rpc::merkle_tree::MerkleTreeExt, +}; +use light_program_test::{ + indexer::{TestIndexer, TestIndexerExtensions}, + test_env::{setup_test_programs_with_accounts_v2, EnvAccounts}, + test_rpc::ProgramTestRpcConnection, +}; +use light_sdk::{ + account_meta::LightAccountMeta, + address::derive_address, + compressed_account::CompressedAccountWithMerkleContext, + instruction_data::LightInstructionData, + merkle_context::{AddressMerkleContext, RemainingAccounts}, + utils::get_cpi_authority_pda, + verify::find_cpi_signer, + PROGRAM_ID_ACCOUNT_COMPRESSION, PROGRAM_ID_LIGHT_SYSTEM, PROGRAM_ID_NOOP, +}; +use light_test_utils::{RpcConnection, RpcError}; +use memo::MemoAccount; +use solana_sdk::{ + instruction::Instruction, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +#[tokio::test] +async fn test_memo_program() { + let (mut rpc, env) = + setup_test_programs_with_accounts_v2(Some(vec![(String::from("memo"), memo::ID)])).await; + let payer = rpc.get_payer().insecure_clone(); + + let mut test_indexer: TestIndexer = TestIndexer::new( + Vec::from(&[StateMerkleTreeAccounts { + merkle_tree: env.merkle_tree_pubkey, + nullifier_queue: env.nullifier_queue_pubkey, + cpi_context: env.cpi_context_account_pubkey, + }]), + Vec::from(&[AddressMerkleTreeAccounts { + merkle_tree: env.address_merkle_tree_pubkey, + queue: env.address_merkle_tree_queue_pubkey, + }]), + payer.insecure_clone(), + env.group_pda.clone(), + None, + ) + .await; + + let mut remaining_accounts = RemainingAccounts::default(); + + let address_merkle_context = AddressMerkleContext { + address_merkle_tree_pubkey: env.address_merkle_tree_pubkey, + address_queue_pubkey: env.address_merkle_tree_queue_pubkey, + }; + + let (address, _) = derive_address( + &[b"memo", payer.pubkey().as_ref()], + &address_merkle_context, + &memo::ID, + ); + + let account_compression_authority = get_cpi_authority_pda(&PROGRAM_ID_LIGHT_SYSTEM); + let registered_program_pda = Pubkey::find_program_address( + &[PROGRAM_ID_LIGHT_SYSTEM.to_bytes().as_slice()], + &PROGRAM_ID_ACCOUNT_COMPRESSION, + ) + .0; + + // Create a memo + let message = "Hello, world!".to_string(); + create_memo( + &message, + &mut rpc, + &mut test_indexer, + &env, + &mut remaining_accounts, + &payer, + &address, + &account_compression_authority, + ®istered_program_pda, + &PROGRAM_ID_LIGHT_SYSTEM, + ) + .await + .unwrap(); + + let compressed_accounts = test_indexer + .get_compressed_accounts_by_owner(&memo::ID) + .await + .unwrap(); + assert_eq!(compressed_accounts.len(), 1); + let compressed_account = &compressed_accounts[0]; + let memo = &compressed_account + .compressed_account + .data + .as_ref() + .unwrap() + .data; + let memo = MemoAccount::deserialize(&mut &memo[..]).unwrap(); + assert_eq!(memo.authority, payer.pubkey()); + assert_eq!(memo.message, "Hello, world!"); + + let new_message = "Updated memo!".to_string(); + update_memo( + &new_message, + &mut rpc, + &mut test_indexer, + &mut remaining_accounts, + &payer, + compressed_account, + &account_compression_authority, + ®istered_program_pda, + &PROGRAM_ID_LIGHT_SYSTEM, + ) + .await + .unwrap(); + + let compressed_accounts = test_indexer + .get_compressed_accounts_by_owner(&memo::ID) + .await + .unwrap(); + assert_eq!(compressed_accounts.len(), 1); + let compressed_account = &compressed_accounts[0]; + let memo = &compressed_account + .compressed_account + .data + .as_ref() + .unwrap() + .data; + let memo = MemoAccount::deserialize(&mut &memo[..]).unwrap(); + assert_eq!(memo.message, "Updated memo!"); + + delete_memo( + &mut rpc, + &mut test_indexer, + &mut remaining_accounts, + &payer, + compressed_account, + &account_compression_authority, + ®istered_program_pda, + &PROGRAM_ID_LIGHT_SYSTEM, + ) + .await + .unwrap(); + + let compressed_accounts = test_indexer + .get_compressed_accounts_by_owner(&memo::ID) + .await + .unwrap(); + assert_eq!(compressed_accounts.len(), 0); +} + +#[allow(clippy::too_many_arguments)] +async fn create_memo( + message: &str, + rpc: &mut R, + test_indexer: &mut TestIndexer, + env: &EnvAccounts, + remaining_accounts: &mut RemainingAccounts, + payer: &Keypair, + address: &[u8; 32], + account_compression_authority: &Pubkey, + registered_program_pda: &Pubkey, + light_system_program: &Pubkey, +) -> Result<(), RpcError> +where + R: RpcConnection + MerkleTreeExt, +{ + let rpc_result = test_indexer + .create_proof_for_compressed_accounts( + None, + None, + Some(&[*address]), + Some(vec![env.address_merkle_tree_pubkey]), + rpc, + ) + .await; + + let address_merkle_context = AddressMerkleContext { + address_merkle_tree_pubkey: env.address_merkle_tree_pubkey, + address_queue_pubkey: env.address_merkle_tree_queue_pubkey, + }; + let account = LightAccountMeta::new_init( + &env.merkle_tree_pubkey, + Some(&address_merkle_context), + Some(rpc_result.address_root_indices[0]), + remaining_accounts, + ) + .unwrap(); + + let inputs = LightInstructionData { + proof: Some(rpc_result), + accounts: Some(vec![account]), + }; + let inputs = inputs.serialize().unwrap(); + let instruction_data = memo::instruction::CreateMemo { + inputs, + message: message.to_string(), + }; + + let cpi_signer = find_cpi_signer(&memo::ID); + + let accounts = memo::accounts::CreateMemo { + signer: payer.pubkey(), + light_system_program: *light_system_program, + account_compression_program: PROGRAM_ID_ACCOUNT_COMPRESSION, + account_compression_authority: *account_compression_authority, + registered_program_pda: *registered_program_pda, + noop_program: PROGRAM_ID_NOOP, + self_program: memo::ID, + cpi_signer, + system_program: solana_sdk::system_program::id(), + }; + + let remaining_accounts = remaining_accounts.to_account_metas(); + + let instruction = Instruction { + program_id: memo::ID, + accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(), + data: instruction_data.data(), + }; + + let event = rpc + .create_and_send_transaction_with_event(&[instruction], &payer.pubkey(), &[payer], None) + .await?; + let slot = rpc.get_slot().await.unwrap(); + test_indexer.add_compressed_accounts_with_token_data(slot, &event.unwrap().0); + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +async fn update_memo( + new_message: &str, + rpc: &mut R, + test_indexer: &mut TestIndexer, + remaining_accounts: &mut RemainingAccounts, + payer: &Keypair, + compressed_account: &CompressedAccountWithMerkleContext, + account_compression_authority: &Pubkey, + registered_program_pda: &Pubkey, + light_system_program: &Pubkey, +) -> Result<(), RpcError> +where + R: RpcConnection + MerkleTreeExt, +{ + let hash = compressed_account.hash().unwrap(); + let merkle_tree_pubkey = compressed_account.merkle_context.merkle_tree_pubkey; + + let rpc_result = test_indexer + .create_proof_for_compressed_accounts( + Some(Vec::from(&[hash])), + Some(Vec::from(&[merkle_tree_pubkey])), + None, + None, + rpc, + ) + .await; + + let compressed_account = LightAccountMeta::new_mut( + compressed_account, + rpc_result.root_indices[0].unwrap(), + &merkle_tree_pubkey, + remaining_accounts, + ); + + let inputs = LightInstructionData { + proof: Some(rpc_result), + accounts: Some(vec![compressed_account]), + }; + let inputs = inputs.serialize().unwrap(); + let instruction_data = memo::instruction::UpdateMemo { + inputs, + new_message: new_message.to_string(), + }; + + let cpi_signer = find_cpi_signer(&memo::ID); + + let accounts = memo::accounts::UpdateMemo { + signer: payer.pubkey(), + light_system_program: *light_system_program, + account_compression_program: PROGRAM_ID_ACCOUNT_COMPRESSION, + account_compression_authority: *account_compression_authority, + registered_program_pda: *registered_program_pda, + noop_program: PROGRAM_ID_NOOP, + self_program: memo::ID, + cpi_signer, + system_program: solana_sdk::system_program::id(), + }; + + let remaining_accounts = remaining_accounts.to_account_metas(); + + let instruction = Instruction { + program_id: memo::ID, + accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(), + data: instruction_data.data(), + }; + + let event = rpc + .create_and_send_transaction_with_event(&[instruction], &payer.pubkey(), &[payer], None) + .await?; + let slot = rpc.get_slot().await.unwrap(); + test_indexer.add_compressed_accounts_with_token_data(slot, &event.unwrap().0); + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +async fn delete_memo( + rpc: &mut R, + test_indexer: &mut TestIndexer, + remaining_accounts: &mut RemainingAccounts, + payer: &Keypair, + compressed_account: &CompressedAccountWithMerkleContext, + account_compression_authority: &Pubkey, + registered_program_pda: &Pubkey, + light_system_program: &Pubkey, +) -> Result<(), RpcError> +where + R: RpcConnection + MerkleTreeExt, +{ + let hash = compressed_account.hash().unwrap(); + let merkle_tree_pubkey = compressed_account.merkle_context.merkle_tree_pubkey; + + let rpc_result = test_indexer + .create_proof_for_compressed_accounts( + Some(Vec::from(&[hash])), + Some(Vec::from(&[merkle_tree_pubkey])), + None, + None, + rpc, + ) + .await; + + let compressed_account = LightAccountMeta::new_close( + compressed_account, + rpc_result.root_indices[0].unwrap(), + remaining_accounts, + ); + + let inputs = LightInstructionData { + proof: Some(rpc_result), + accounts: Some(vec![compressed_account]), + }; + let inputs = inputs.serialize().unwrap(); + let instruction_data = memo::instruction::DeleteMemo { inputs }; + + let cpi_signer = find_cpi_signer(&memo::ID); + + let accounts = memo::accounts::DeleteMemo { + signer: payer.pubkey(), + light_system_program: *light_system_program, + account_compression_program: PROGRAM_ID_ACCOUNT_COMPRESSION, + account_compression_authority: *account_compression_authority, + registered_program_pda: *registered_program_pda, + noop_program: PROGRAM_ID_NOOP, + self_program: memo::ID, + cpi_signer, + system_program: solana_sdk::system_program::id(), + }; + + let remaining_accounts = remaining_accounts.to_account_metas(); + + let instruction = Instruction { + program_id: memo::ID, + accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(), + data: instruction_data.data(), + }; + + let event = rpc + .create_and_send_transaction_with_event(&[instruction], &payer.pubkey(), &[payer], None) + .await?; + let slot = rpc.get_slot().await.unwrap(); + test_indexer.add_compressed_accounts_with_token_data(slot, &event.unwrap().0); + Ok(()) +} diff --git a/examples/memo/tsconfig.json b/examples/memo/tsconfig.json new file mode 100644 index 0000000000..cd5d2e3d06 --- /dev/null +++ b/examples/memo/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } +}