diff --git a/cross-chain/solana/.gitignore b/cross-chain/solana/.gitignore new file mode 100644 index 000000000..a5637d55c --- /dev/null +++ b/cross-chain/solana/.gitignore @@ -0,0 +1,9 @@ +*.js +*.so +*.key +*.json +!tsconfig.json +!package.json +node_modules +yarn.lock +/test-ledger diff --git a/cross-chain/solana/README.adoc b/cross-chain/solana/README.adoc new file mode 100644 index 000000000..f425eb9c8 --- /dev/null +++ b/cross-chain/solana/README.adoc @@ -0,0 +1,56 @@ +:toc: macro + += Threshold cross-chain - Solana + +This package brings Bitcoin to Solana. For more details please +see link:https://github.com/keep-network/tbtc-v2/blob/main/docs/rfc/rfc-8.adoc[RFC 8: Cross-chain Tokenized Threshold BTC] + +== How it works? + +``` ++----------------------------+ +-----------------------------------------------------------------------+ +| Ethereum | | Solana | +| | | | +| +----------------------+ | | +----------------------+ +-----------------------+ +------------+ | +| | Wormhole TokenBridge |--|---------|--| Wormhole TokenBridge |--| SolanaWormholeGateway |--| SolanaTBTC | | +| +----------------------+ | | +----------------------+ +-----------------------+ +------------+ | +| | | | ++----------------------------+ +-----------------------------------------------------------------------+ +``` + +- `SolanaTBTC` canonical tBTC token on Solana with a minting authority +delegated to `SolanaWormholeGateway`. +- `SolanaWormholeGateway` is a smart contract wrapping and unwrapping +Wormhole-specific tBTC representation into the canonical `SolanaTBTC` token. + +=== Local development + +For testing and debugging purposes it is convinient to run Solana cluster locally. + +link:https://docs.solana.com/cli/install-solana-cli-tools#use-solanas-install-tool[Install] Solana Tool Suite + +In terminal run `solana-test-validator`. For more details see link:https://docs.solana.com/developing/test-validator[Solana Test Validator] + +==== Running tests + +Navigate to `/cross-chain/solana` and run `runTests.sh`. This script compiles Solidity contract(s) to produce Solana artifacts. + +=== Updating Wormhole Gateway mapping + +TODO: add + +=== Deploy contracts + +TODO: add + +=== Contract upgrades + +Supported out of the box. See https://docs.solana.com/cli/deploy-a-program#redeploy-a-program + +TODO: add examples + +=== Freezing and thawing token account + +Supported with a freezing authority passed as an argument during token creation. + +TODO: add examples \ No newline at end of file diff --git a/cross-chain/solana/SolanaTBTC.sol b/cross-chain/solana/SolanaTBTC.sol new file mode 100644 index 000000000..16c8e4aeb --- /dev/null +++ b/cross-chain/solana/SolanaTBTC.sol @@ -0,0 +1,119 @@ +import './solana-library/spl_token.sol'; +import 'solana'; + +contract SolanaTBTC { + /// @notice Indicates if the given address is a minter. Only minters can + /// mint the token. + mapping(address => bool) isMinter; + + /// @notice List of all minters. + address[] minters; + + /// @notice owner + address authority; + + /// @notice mint account + /// @dev Stores information about the token itself. E.g. current supply and + /// its authorities + address mint; + + event MinterAdded(address minter); + event MinterRemoved(address minter); + + modifier needs_authority() { + for (uint64 i = 0; i < tx.accounts.length; i++) { + AccountInfo ai = tx.accounts[i]; + if (ai.key == authority && ai.is_signer) { + _; + return; + } + } + + print("Not signed by authority"); + revert("Not signed by authority"); + } + + modifier minter_only() { + for (uint64 i = 0; i < tx.accounts.length; i++) { + AccountInfo ai = tx.accounts[i]; + print("checking if a minter"); + if (isMinter[ai.key] && ai.is_signer) { + _; + return; + } + } + + print("Not a minter"); + revert("Not a minter"); + } + + constructor(address initial_authority) { + authority = initial_authority; + } + + /// @notice Adds the address to the minters list. + /// @dev Requirements: + /// - The caller must have authority. + /// - `minter` must not be a minter address already. + /// @param minter The address to be added as a minter. This address can mint + /// SolanaTBTC token. + function add_minter(address minter) needs_authority public { + print("adding a minter..."); + require(!isMinter[minter], "This address is already a minter"); + isMinter[minter] = true; + minters.push(minter); + emit MinterAdded(minter); + print("added a minter..."); + } + + /// @notice Removes the address from the minters list. + /// @dev Requirements: + /// - The caller must have authority. + /// - `minter` must be a minter address. + /// @param minter The address to be removed from the minters list. + function remove_minter(address minter) public needs_authority { + require(isMinter[minter], "This address is not a minter"); + delete isMinter[minter]; + + // We do not expect too many minters so a simple loop is safe. + for (uint256 i = 0; i < minters.length; i++) { + if (minters[i] == minter) { + minters[i] = minters[minters.length - 1]; + minters.pop(); + break; + } + } + + emit MinterRemoved(minter); + } + + function set_mint(address _mint) needs_authority public { + mint = _mint; + } + + function mint_to(address account, address _authority, uint64 amount) minter_only public { + print("yo, I'm in mint_to"); + SplToken.mint_to(mint, account, _authority, amount); + } + + function total_supply() public view returns (uint64) { + return SplToken.total_supply(mint); + } + + function get_balance(address account) public view returns (uint64) { + return SplToken.get_balance(account); + } + + function transfer(address from, address to, address owner, uint64 amount) public { + SplToken.transfer(from, to, owner, amount); + } + + function burn(address account, address owner, uint64 amount) public { + SplToken.burn(account, mint, owner, amount); + } + + /// @notice Allows to fetch a list of all minters. + function get_minters() public view returns (address[] memory) { + return minters; + } +} \ No newline at end of file diff --git a/cross-chain/solana/SolanaTBTC.spec.ts b/cross-chain/solana/SolanaTBTC.spec.ts new file mode 100644 index 000000000..94f910542 --- /dev/null +++ b/cross-chain/solana/SolanaTBTC.spec.ts @@ -0,0 +1,378 @@ +import { + getOrCreateAssociatedTokenAccount, + createMint, + Account, +} from "@solana/spl-token" +import { Keypair, PublicKey, Connection } from "@solana/web3.js" +import { Program, Provider, BN } from "@project-serum/anchor" +import expect from "expect" +import { loadContract, loadKey } from "./setup" + +describe("SolanaTBTC token", function () { + this.timeout(500000) + + let program: Program + let storage: Keypair + let payer: Keypair + let thirdParty: Keypair + let provider: Provider + let authority: Keypair + let guardianAuthority: Keypair + let connection: Connection + + before(async () => { + payer = loadKey("payer.key") // SolanaTBTC authority (deployer) + thirdParty = loadKey("thirdParty.key") // third party account + authority = Keypair.generate() // mint authority + guardianAuthority = Keypair.generate() // guardians can freeze minting + ;({ provider, storage, program } = await loadContract("SolanaTBTC", [ + payer.publicKey, + ])) + + connection = provider.connection + }) + + describe("adding and removing minters", () => { + it("should check for empty minters array", async () => { + const minters = await program.methods + .getMinters() + .accounts({ dataAccount: storage.publicKey }) + .view() + + expect(minters.length).toEqual(0) + }) + + it("should add a minter", async () => { + await program.methods + .addMinter(authority.publicKey) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([payer]) + .rpc() + + const minters = await program.methods + .getMinters() + .accounts({ dataAccount: storage.publicKey }) + .view() + + expect(minters.length).toEqual(1) + expect(minters[0]).toEqual(authority.publicKey) + }) + + it("should remove a minter", async () => { + await program.methods + .removeMinter(authority.publicKey) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([payer]) + .rpc() + + const minters = await program.methods + .getMinters() + .accounts({ dataAccount: storage.publicKey }) + .view() + + expect(minters.length).toEqual(0) + }) + }) + + describe("when a caller has no `mint setting` authority", () => { + it("should revert", async () => { + const mint = await createMint( + connection, + payer, + authority.publicKey, // mint authority + guardianAuthority.publicKey, // freeze authority + 18 // 18 decimals like TBTC on Ethereum + ) + + // third party is not the owner (authority) + try { + await program.methods + .setMint(mint) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: thirdParty.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([thirdParty]) + .rpc() + } catch (e: any) { + expect(e.logs).toContain("Program log: Not signed by authority") + } + }) + }) + + describe("when a caller has no 'adding a minter' authority", () => { + it("should revert", async () => { + // third party has no authority + try { + await program.methods + .addMinter(authority.publicKey) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: thirdParty.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([thirdParty]) + .rpc() + } catch (e: any) { + expect(e.logs).toContain("Program log: Not signed by authority") + } + }) + }) + + describe("when a caller is not a minter", () => { + it("should revert", async () => { + const mint = await createMint( + connection, + payer, + authority.publicKey, // mint authority + guardianAuthority.publicKey, // freeze authority + 18 // 18 decimals like TBTC on Ethereum + ) + + const tokenAccount = await getOrCreateAssociatedTokenAccount( + connection, + payer, + mint, + payer.publicKey + ) + + await program.methods + .addMinter(payer.publicKey) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([payer]) + .rpc() + + try { + await program.methods + .mintTo(tokenAccount.address, thirdParty.publicKey, new BN(100000)) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: mint, isSigner: false, isWritable: true }, + { pubkey: tokenAccount.address, isSigner: false, isWritable: true }, + { pubkey: thirdParty.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([thirdParty]) + .rpc() + } catch (e: any) { + expect(e.logs).toContain("Program log: Not a minter") + } + }) + }) + + describe("when callers have authority", () => { + let mint: PublicKey + let tokenAccount: Account + let otherTokenAccount: Account + let balance: BN + let totalSupply: BN + + it("should create a mint account and a token account with 0 balance", async () => { + mint = await createMint( + connection, + payer, + authority.publicKey, // mint authority + guardianAuthority.publicKey, // aka freeze authority + 18 // 18 decimals like TBTC on Ethereum + ) + + // payer is the owner (authority) + await program.methods + .setMint(mint) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([payer]) + .rpc() + + totalSupply = await program.methods + .totalSupply() + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: mint, isSigner: false, isWritable: false }, + ]) + .view() + + expect(totalSupply.toNumber()).toBe(0) + + tokenAccount = await getOrCreateAssociatedTokenAccount( + connection, + payer, + mint, + payer.publicKey + ) + + balance = await program.methods + .getBalance(tokenAccount.address) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: tokenAccount.address, isSigner: false, isWritable: false }, + ]) + .view() + + expect(balance.toNumber()).toBe(0) + }) + + it("should mint tokens", async () => { + await program.methods + .addMinter(authority.publicKey) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([payer]) + .rpc() + + await program.methods + .mintTo(tokenAccount.address, authority.publicKey, new BN(100000)) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: mint, isSigner: false, isWritable: true }, + { pubkey: tokenAccount.address, isSigner: false, isWritable: true }, + { pubkey: authority.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([authority]) + .rpc() + + // check the balances + totalSupply = await program.methods + .totalSupply() + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: mint, isSigner: false, isWritable: false }, + ]) + .view() + + expect(totalSupply.toNumber()).toBe(100000) + balance = await program.methods + .getBalance(tokenAccount.address) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: tokenAccount.address, isSigner: false, isWritable: false }, + ]) + .view() + + expect(balance.toNumber()).toBe(100000) + }) + + it("should transfer tokens", async () => { + otherTokenAccount = await getOrCreateAssociatedTokenAccount( + connection, + payer, + mint, + thirdParty.publicKey + ) + + await program.methods + .transfer( + tokenAccount.address, + otherTokenAccount.address, + payer.publicKey, + new BN(70000) + ) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { + pubkey: otherTokenAccount.address, + isSigner: false, + isWritable: true, + }, + { pubkey: tokenAccount.address, isSigner: false, isWritable: true }, + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([payer]) + .rpc() + + totalSupply = await program.methods + .totalSupply() + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: mint, isSigner: false, isWritable: false }, + ]) + .view() + + expect(totalSupply.toNumber()).toBe(100000) + balance = await program.methods + .getBalance(tokenAccount.address) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: tokenAccount.address, isSigner: false, isWritable: false }, + ]) + .view() + + expect(balance.toNumber()).toBe(30000) + + balance = await program.methods + .getBalance(otherTokenAccount.address) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { + pubkey: otherTokenAccount.address, + isSigner: false, + isWritable: false, + }, + ]) + .view() + + expect(balance.toNumber()).toBe(70000) + }) + + it("should burn tokens", async () => { + await program.methods + .burn(otherTokenAccount.address, thirdParty.publicKey, new BN(20000)) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { + pubkey: otherTokenAccount.address, + isSigner: false, + isWritable: true, + }, + { pubkey: mint, isSigner: false, isWritable: true }, + { pubkey: thirdParty.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([thirdParty]) + .rpc() + + totalSupply = await program.methods + .totalSupply() + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: mint, isSigner: false, isWritable: false }, + ]) + .view() + + expect(totalSupply.toNumber()).toBe(80000) + balance = await program.methods + .getBalance(tokenAccount.address) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: tokenAccount.address, isSigner: false, isWritable: false }, + ]) + .view() + + expect(balance.toNumber()).toBe(30000) + + balance = await program.methods + .getBalance(otherTokenAccount.address) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { + pubkey: otherTokenAccount.address, + isSigner: false, + isWritable: false, + }, + ]) + .view() + + expect(balance.toNumber()).toBe(50000) + }) + }) +}) diff --git a/cross-chain/solana/package.json b/cross-chain/solana/package.json new file mode 100644 index 000000000..baf52087d --- /dev/null +++ b/cross-chain/solana/package.json @@ -0,0 +1,40 @@ +{ + "name": "solana-tests", + "version": "0.0.1", + "description": "Integration tests with Solang and Solana", + "scripts": { + "test": "tsc; ts-node setup.ts; mocha *.spec.ts", + "build": "solang compile *.sol --target solana -v", + "format": "npm run lint", + "format:fix": "npm run lint:fix", + "lint": "npm run lint:eslint", + "lint:fix": "npm run lint:fix:eslint", + "lint:eslint": "eslint .", + "lint:fix:eslint": "eslint . --fix" + }, + "author": "Sean Young ", + "license": "MIT", + "devDependencies": { + "@types/mocha": "^9.1.0", + "@types/node": "^14.14.10", + "expect": "^26.6.2", + "mocha": "^9.1.0", + "ts-node": "^10.4.0", + "typescript": "^4.1.2", + "prettier": "^2.5.1", + "eslint": "^7.32.0", + "@thesis-co/eslint-config": "github:thesis/eslint-config", + "prettier-plugin-sh": "^0.8.1", + "prettier-plugin-solidity": "^1.0.0-beta.19" + }, + "dependencies": { + "@project-serum/anchor": "^0.26", + "@solana/spl-token": "0.2.0", + "@solana/web3.js": "^1.68", + "ethers": "^5.2.0", + "fast-sha256": "^1.3.0", + "tweetnacl": "^1.0.3", + "web3-eth-abi": "^1.3.0", + "web3-utils": "^1.3.0" + } +} \ No newline at end of file diff --git a/cross-chain/solana/runTests.sh b/cross-chain/solana/runTests.sh new file mode 100755 index 000000000..e0dd9b51d --- /dev/null +++ b/cross-chain/solana/runTests.sh @@ -0,0 +1,7 @@ +#!/bin/bash +rm -rf SolanaTBTC.json +rm -rf SolanaTBTC.so + +solang compile -v --target solana SolanaTBTC.sol + +yarn test \ No newline at end of file diff --git a/cross-chain/solana/setup.ts b/cross-chain/solana/setup.ts new file mode 100644 index 000000000..a5b7acaf4 --- /dev/null +++ b/cross-chain/solana/setup.ts @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { + Connection, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + BpfLoader, + Transaction, + SystemProgram, + BPF_LOADER_PROGRAM_ID, + TransactionExpiredBlockheightExceededError, +} from "@solana/web3.js" +import { AnchorProvider, Program } from "@project-serum/anchor" +import fs from "fs" + +const endpoint: string = process.env.RPC_URL || "http://127.0.0.1:8899" + +export async function loadContract( + name: string, + args: any[] = [], + space = 8192 +): Promise<{ + program: Program + provider: AnchorProvider + storage: Keypair + programKey: PublicKey +}> { + const idl = JSON.parse(fs.readFileSync(`${name}.json`, "utf8")) + + process.env.ANCHOR_WALLET = "payer.key" + + const provider = AnchorProvider.local(endpoint) + + const storage = Keypair.generate() + + const programKey = loadKey(`${name}.key`) + + await createAccount(storage, programKey.publicKey, space) + + const program = new Program(idl, programKey.publicKey, provider) + + await program.methods + .new(...args) + .accounts({ dataAccount: storage.publicKey }) + .rpc() + + return { provider, program, storage, programKey: programKey.publicKey } +} + +export async function createAccount( + account: Keypair, + programId: PublicKey, + space: number +) { + const provider = AnchorProvider.local(endpoint) + const lamports = await provider.connection.getMinimumBalanceForRentExemption( + space + ) + + const transaction = new Transaction() + + transaction.add( + SystemProgram.createAccount({ + fromPubkey: provider.wallet.publicKey, + newAccountPubkey: account.publicKey, + lamports, + space, + programId, + }) + ) + + await provider.sendAndConfirm(transaction, [account]) +} + +export function newConnectionAndPayer(): [Connection, Keypair] { + const connection = newConnection() + const payerAccount = loadKey("payer.key") + return [connection, payerAccount] +} + +export async function loadContractWithProvider( + provider: AnchorProvider, + name: string, + args: any[] = [], + space = 8192 +): Promise<{ program: Program; storage: Keypair; programKey: PublicKey }> { + const idl = JSON.parse(fs.readFileSync(`${name}.json`, "utf8")) + + const storage = Keypair.generate() + const programKey = loadKey(`${name}.key`) + + await createAccount(storage, programKey.publicKey, space) + + const program = new Program(idl, programKey.publicKey, provider) + + await program.methods + .new(...args) + .accounts({ dataAccount: storage.publicKey }) + .rpc() + + return { program, storage, programKey: programKey.publicKey } +} + +export function loadKey(filename: string): Keypair { + const contents = fs.readFileSync(filename).toString() + const bs = Uint8Array.from(JSON.parse(contents)) + + return Keypair.fromSecretKey(bs) +} + +async function newAccountWithLamports( + connection: Connection +): Promise { + const account = Keypair.generate() + + console.log("Airdropping SOL to a new wallet ...") + const signature = await connection.requestAirdrop( + account.publicKey, + 100 * LAMPORTS_PER_SOL + ) + const latestBlockHash = await connection.getLatestBlockhash() + + await connection.confirmTransaction( + { + blockhash: latestBlockHash.blockhash, + lastValidBlockHeight: latestBlockHash.lastValidBlockHeight, + signature, + }, + "confirmed" + ) + + return account +} + +async function setup() { + const writeKey = (file_name: string, key: Keypair) => { + fs.writeFileSync(file_name, JSON.stringify(Array.from(key.secretKey))) + } + + let connection = newConnection() + const payer = await newAccountWithLamports(connection) + const thirdParty = await newAccountWithLamports(connection) + + writeKey("payer.key", payer) + writeKey("thirdParty.key", thirdParty) + + const files = fs.readdirSync(__dirname) + for (const index in files) { + const file = files[index] + + if (file.endsWith(".so")) { + const name = file.slice(0, -3) + let program + + if (fs.existsSync(`${name}.key`)) { + program = loadKey(`${name}.key`) + } else { + program = Keypair.generate() + } + + console.log(`Loading ${name} at ${program.publicKey}...`) + const programSo = fs.readFileSync(file) + for (let retries = 5; retries > 0; retries -= 1) { + try { + await BpfLoader.load( + connection, + payer, + program, + programSo, + BPF_LOADER_PROGRAM_ID + ) + break + } catch (e) { + if (e instanceof TransactionExpiredBlockheightExceededError) { + console.log(e) + console.log("retrying...") + connection = newConnection() + } else { + throw e + } + } + } + console.log(`Done loading ${name} ...`) + + writeKey(`${name}.key`, program) + } + } + + // If there was a TransactionExpiredBlockheightExceededError exception, then + // setup.ts does not exit. I have no idea why + process.exit() +} + +function newConnection(): Connection { + return new Connection(endpoint, { + commitment: "confirmed", + confirmTransactionInitialTimeout: 1e6, + }) +} + +if (require.main === module) { + (async () => { + await setup() + })() +} diff --git a/cross-chain/solana/solana-library/spl_token.sol b/cross-chain/solana/solana-library/spl_token.sol new file mode 100644 index 000000000..4f961fb29 --- /dev/null +++ b/cross-chain/solana/solana-library/spl_token.sol @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Disclaimer: This library provides a way for Solidity to interact with Solana's SPL-Token. Although it is production ready, +// it has not been audited for security, so use it at your own risk. + +import 'solana'; + +library SplToken { + address constant tokenProgramId = address"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; + enum TokenInstruction { + InitializeMint, // 0 + InitializeAccount, // 1 + InitializeMultisig, // 2 + Transfer, // 3 + Approve, // 4 + Revoke, // 5 + SetAuthority, // 6 + MintTo, // 7 + Burn, // 8 + CloseAccount, // 9 + FreezeAccount, // 10 + ThawAccount, // 11 + TransferChecked, // 12 + ApproveChecked, // 13 + MintToChecked, // 14 + BurnChecked, // 15 + InitializeAccount2, // 16 + SyncNative, // 17 + InitializeAccount3, // 18 + InitializeMultisig2, // 19 + InitializeMint2, // 20 + GetAccountDataSize, // 21 + InitializeImmutableOwner, // 22 + AmountToUiAmount, // 23 + UiAmountToAmount, // 24 + InitializeMintCloseAuthority, // 25 + TransferFeeExtension, // 26 + ConfidentialTransferExtension, // 27 + DefaultAccountStateExtension, // 28 + Reallocate, // 29 + MemoTransferExtension, // 30 + CreateNativeMint // 31 + } + + /// Mint new tokens. The transaction should be signed by the mint authority keypair + /// + /// @param mint the account of the mint + /// @param account the token account where the minted tokens should go + /// @param authority the public key of the mint authority + /// @param amount the amount of tokens to mint + function mint_to(address mint, address account, address authority, uint64 amount) internal { + bytes instr = new bytes(9); + + instr[0] = uint8(TokenInstruction.MintTo); + instr.writeUint64LE(amount, 1); + + AccountMeta[3] metas = [ + AccountMeta({pubkey: mint, is_writable: true, is_signer: false}), + AccountMeta({pubkey: account, is_writable: true, is_signer: false}), + AccountMeta({pubkey: authority, is_writable: true, is_signer: true}) + ]; + + tokenProgramId.call{accounts: metas}(instr); + } + + /// Transfer @amount token from @from to @to. The transaction should be signed by the owner + /// keypair of the from account. + /// + /// @param from the account to transfer tokens from + /// @param to the account to transfer tokens to + /// @param owner the publickey of the from account owner keypair + /// @param amount the amount to transfer + function transfer(address from, address to, address owner, uint64 amount) internal { + bytes instr = new bytes(9); + + instr[0] = uint8(TokenInstruction.Transfer); + instr.writeUint64LE(amount, 1); + + AccountMeta[3] metas = [ + AccountMeta({pubkey: from, is_writable: true, is_signer: false}), + AccountMeta({pubkey: to, is_writable: true, is_signer: false}), + AccountMeta({pubkey: owner, is_writable: true, is_signer: true}) + ]; + + tokenProgramId.call{accounts: metas}(instr); + } + + /// Burn @amount tokens in account. This transaction should be signed by the owner. + /// + /// @param account the acount for which tokens should be burned + /// @param mint the mint for this token + /// @param owner the publickey of the account owner keypair + /// @param amount the amount to transfer + function burn(address account, address mint, address owner, uint64 amount) internal { + bytes instr = new bytes(9); + + instr[0] = uint8(TokenInstruction.Burn); + instr.writeUint64LE(amount, 1); + + AccountMeta[3] metas = [ + AccountMeta({pubkey: account, is_writable: true, is_signer: false}), + AccountMeta({pubkey: mint, is_writable: true, is_signer: false}), + AccountMeta({pubkey: owner, is_writable: true, is_signer: true}) + ]; + + tokenProgramId.call{accounts: metas}(instr); + } + + /// Approve an amount to a delegate. This transaction should be signed by the owner + /// + /// @param account the account for which a delegate should be approved + /// @param delegate the delegate publickey + /// @param owner the publickey of the account owner keypair + /// @param amount the amount to approve + function approve(address account, address delegate, address owner, uint64 amount) internal { + bytes instr = new bytes(9); + + instr[0] = uint8(TokenInstruction.Approve); + instr.writeUint64LE(amount, 1); + + AccountMeta[3] metas = [ + AccountMeta({pubkey: account, is_writable: true, is_signer: false}), + AccountMeta({pubkey: delegate, is_writable: false, is_signer: false}), + AccountMeta({pubkey: owner, is_writable: false, is_signer: true}) + ]; + + tokenProgramId.call{accounts: metas}(instr); + } + + /// Revoke a previously approved delegate. This transaction should be signed by the owner. After + /// this transaction, no delgate is approved for any amount. + /// + /// @param account the account for which a delegate should be approved + /// @param owner the publickey of the account owner keypair + function revoke(address account, address owner) internal { + bytes instr = new bytes(1); + + instr[0] = uint8(TokenInstruction.Revoke); + + AccountMeta[2] metas = [ + AccountMeta({pubkey: account, is_writable: true, is_signer: false}), + AccountMeta({pubkey: owner, is_writable: false, is_signer: true}) + ]; + + tokenProgramId.call{accounts: metas}(instr); + } + + /// Get the total supply for the mint, i.e. the total amount in circulation + /// @param mint the mint for this token + function total_supply(address mint) internal view returns (uint64) { + AccountInfo account = get_account_info(mint); + + return account.data.readUint64LE(36); + } + + /// Get the balance for an account. + /// + /// @param account the account for which we want to know a balance + function get_balance(address account) internal view returns (uint64) { + AccountInfo ai = get_account_info(account); + + return ai.data.readUint64LE(64); + } + + /// Get the account info for an account. This walks the transaction account infos + /// and find the account info, or the transaction fails. + /// + /// @param account the account for which we want to have the acount info. + function get_account_info(address account) internal view returns (AccountInfo) { + for (uint64 i = 0; i < tx.accounts.length; i++) { + AccountInfo ai = tx.accounts[i]; + if (ai.key == account) { + return ai; + } + } + + revert("account missing"); + } + + /// This enum represents the state of a token account + enum AccountState { + Uninitialized, + Initialized, + Frozen + } + + /// This struct is the return of 'get_token_account_data' + struct TokenAccountData { + address mintAccount; + address owner; + uint64 balance; + bool delegate_present; + address delegate; + AccountState state; + bool is_native_present; + uint64 is_native; + uint64 delegated_amount; + bool close_authority_present; + address close_authority; + } + + /// Fetch the owner, mint account and balance for an associated token account. + /// + /// @param tokenAccount The token account + /// @return struct TokenAccountData + function get_token_account_data(address tokenAccount) public view returns (TokenAccountData) { + AccountInfo ai = get_account_info(tokenAccount); + + TokenAccountData data = TokenAccountData( + { + mintAccount: ai.data.readAddress(0), + owner: ai.data.readAddress(32), + balance: ai.data.readUint64LE(64), + delegate_present: ai.data.readUint32LE(72) > 0, + delegate: ai.data.readAddress(76), + state: AccountState(ai.data[108]), + is_native_present: ai.data.readUint32LE(109) > 0, + is_native: ai.data.readUint64LE(113), + delegated_amount: ai.data.readUint64LE(121), + close_authority_present: ai.data.readUint32LE(129) > 10, + close_authority: ai.data.readAddress(133) + } + ); + + return data; + } + + // This struct is the return of 'get_mint_account_data' + struct MintAccountData { + bool authority_present; + address mint_authority; + uint64 supply; + uint8 decimals; + bool is_initialized; + bool freeze_authority_present; + address freeze_authority; + } + + /// Retrieve the information saved in a mint account + /// + /// @param mintAccount the account whose information we want to retrive + /// @return the MintAccountData struct + function get_mint_account_data(address mintAccount) public view returns (MintAccountData) { + AccountInfo ai = get_account_info(mintAccount); + + uint32 authority_present = ai.data.readUint32LE(0); + uint32 freeze_authority_present = ai.data.readUint32LE(46); + MintAccountData data = MintAccountData( { + authority_present: authority_present > 0, + mint_authority: ai.data.readAddress(4), + supply: ai.data.readUint64LE(36), + decimals: uint8(ai.data[44]), + is_initialized: ai.data[45] > 0, + freeze_authority_present: freeze_authority_present > 0, + freeze_authority: ai.data.readAddress(50) + }); + + return data; + } + + // A mint account has an authority, whose type is one of the members of this struct. + enum AuthorityType { + MintTokens, + FreezeAccount, + AccountOwner, + CloseAccount + } + + /// Remove the mint authority from a mint account + /// + /// @param mintAccount the public key for the mint account + /// @param mintAuthority the public for the mint authority + function remove_mint_authority(address mintAccount, address mintAuthority) public { + AccountMeta[2] metas = [ + AccountMeta({pubkey: mintAccount, is_signer: false, is_writable: true}), + AccountMeta({pubkey: mintAuthority, is_signer: true, is_writable: false}) + ]; + + bytes data = new bytes(9); + data[0] = uint8(TokenInstruction.SetAuthority); + data[1] = uint8(AuthorityType.MintTokens); + data[3] = 0; + + tokenProgramId.call{accounts: metas}(data); + } +} diff --git a/cross-chain/solana/solana-library/system_instruction.sol b/cross-chain/solana/solana-library/system_instruction.sol new file mode 100644 index 000000000..0ba370c8c --- /dev/null +++ b/cross-chain/solana/solana-library/system_instruction.sol @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Disclaimer: This library provides a bridge for Solidity to interact with Solana's system instructions. Although it is production ready, +// it has not been audited for security, so use it at your own risk. + +import 'solana'; + +library SystemInstruction { + address constant systemAddress = address"11111111111111111111111111111111"; + address constant recentBlockHashes = address"SysvarRecentB1ockHashes11111111111111111111"; + address constant rentAddress = address"SysvarRent111111111111111111111111111111111"; + uint64 constant state_size = 80; + + enum Instruction { + CreateAccount, + Assign, + Transfer, + CreateAccountWithSeed, + AdvanceNounceAccount, + WithdrawNonceAccount, + InitializeNonceAccount, + AuthorizeNonceAccount, + Allocate, + AllocateWithSeed, + AssignWithSeed, + TransferWithSeed, + UpgradeNonceAccount // This is not available on Solana v1.9.15 + } + + /// Create a new account on Solana + /// + /// @param from public key for the account from which to transfer lamports to the new account + /// @param to public key for the account to be created + /// @param lamports amount of lamports to be transfered to the new account + /// @param space the size in bytes that is going to be made available for the account + /// @param owner public key for the program that will own the account being created + function create_account(address from, address to, uint64 lamports, uint64 space, address owner) internal { + AccountMeta[2] metas = [ + AccountMeta({pubkey: from, is_signer: true, is_writable: true}), + AccountMeta({pubkey: to, is_signer: true, is_writable: true}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.CreateAccount), lamports, space, owner); + + systemAddress.call{accounts: metas}(bincode); + } + + /// Create a new account on Solana using a public key derived from a seed + /// + /// @param from public key for the account from which to transfer lamports to the new account + /// @param to the public key for the account to be created. The public key must match create_with_seed(base, seed, owner) + /// @param base the base address that derived the 'to' address using the seed + /// @param seed the string utilized to created the 'to' public key + /// @param lamports amount of lamports to be transfered to the new account + /// @param space the size in bytes that is going to be made available for the account + /// @param owner public key for the program that will own the account being created + function create_account_with_seed(address from, address to, address base, string seed, uint64 lamports, uint64 space, address owner) internal { + AccountMeta[3] metas = [ + AccountMeta({pubkey: from, is_signer: true, is_writable: true}), + AccountMeta({pubkey: to, is_signer: false, is_writable: true}), + AccountMeta({pubkey: base, is_signer: true, is_writable: false}) + ]; + + uint32 buffer_size = 92 + seed.length; + bytes bincode = new bytes(buffer_size); + bincode.writeUint32LE(uint32(Instruction.CreateAccountWithSeed), 0); + bincode.writeAddress(base, 4); + bincode.writeUint64LE(uint64(seed.length), 36); + bincode.writeString(seed, 44); + uint32 offset = seed.length + 44; + bincode.writeUint64LE(lamports, offset); + offset += 8; + bincode.writeUint64LE(space, offset); + offset += 8; + bincode.writeAddress(owner, offset); + + systemAddress.call{accounts: metas}(bincode); + } + + /// Assign account to a program (owner) + /// + /// @param pubkey the public key for the account whose owner is going to be reassigned + /// @param owner the public key for the new account owner + function assign(address pubkey, address owner) internal { + AccountMeta[1] meta = [ + AccountMeta({pubkey: pubkey, is_signer: true, is_writable: true}) + ]; + bytes bincode = abi.encode(uint32(Instruction.Assign), owner); + + systemAddress.call{accounts: meta}(bincode); + } + + /// Assign account to a program (owner) based on a seed + /// + /// @param addr the public key for the account whose owner is going to be reassigned. The public key must match create_with_seed(base, seed, owner) + /// @param base the base address that derived the 'addr' key using the seed + /// @param seed the string utilized to created the 'addr' public key + /// @param owner the public key for the new program owner + function assign_with_seed(address addr, address base, string seed, address owner) internal { + AccountMeta[2] metas = [ + AccountMeta({pubkey: addr, is_signer: false, is_writable: true}), + AccountMeta({pubkey: base, is_signer: true, is_writable: false}) + ]; + + + uint32 buffer_size = 76 + seed.length; + bytes bincode = new bytes(buffer_size); + bincode.writeUint32LE(uint32(Instruction.AssignWithSeed), 0); + bincode.writeAddress(base, 4); + bincode.writeUint64LE(uint64(seed.length), 36); + bincode.writeString(seed, 44); + bincode.writeAddress(owner, 44 + seed.length); + + systemAddress.call{accounts: metas}(bincode); + } + + /// Transfer lamports between accounts + /// + /// @param from public key for the funding account + /// @param to public key for the recipient account + /// @param lamports amount of lamports to transfer + function transfer(address from, address to, uint64 lamports) internal { + AccountMeta[2] metas = [ + AccountMeta({pubkey: from, is_signer: true, is_writable: true}), + AccountMeta({pubkey: to, is_signer: false, is_writable: true}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.Transfer), lamports); + + systemAddress.call{accounts: metas}(bincode); + } + + /// Transfer lamports from a derived address + /// + /// @param from_pubkey The funding account public key. It should match create_with_seed(from_base, seed, from_owner) + /// @param from_base the base address that derived the 'from_pubkey' key using the seed + /// @param seed the string utilized to create the 'from_pubkey' public key + /// @param from_owner owner to use to derive the funding account address + /// @param to_pubkey the public key for the recipient account + /// @param lamports amount of lamports to transfer + function transfer_with_seed(address from_pubkey, address from_base, string seed, address from_owner, address to_pubkey, uint64 lamports) internal { + AccountMeta[3] metas = [ + AccountMeta({pubkey: from_pubkey, is_signer: false, is_writable: true}), + AccountMeta({pubkey: from_base, is_signer: true, is_writable: false}), + AccountMeta({pubkey: to_pubkey, is_signer: false, is_writable: true}) + ]; + + uint32 buffer_size = seed.length + 52; + bytes bincode = new bytes(buffer_size); + bincode.writeUint32LE(uint32(Instruction.TransferWithSeed), 0); + bincode.writeUint64LE(lamports, 4); + bincode.writeUint64LE(seed.length, 12); + bincode.writeString(seed, 20); + bincode.writeAddress(from_owner, 20 + seed.length); + + systemAddress.call{accounts: metas}(bincode); + } + + /// Allocate space in a (possibly new) account without funding + /// + /// @param pub_key account for which to allocate space + /// @param space number of bytes of memory to allocate + function allocate(address pub_key, uint64 space) internal { + AccountMeta[1] meta = [ + AccountMeta({pubkey: pub_key, is_signer: true, is_writable: true}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.Allocate), space); + + systemAddress.call{accounts: meta}(bincode); + } + + /// Allocate space for an assign an account at an address derived from a base public key and a seed + /// + /// @param addr account for which to allocate space. It should match create_with_seed(base, seed, owner) + /// @param base the base address that derived the 'addr' key using the seed + /// @param seed the string utilized to create the 'addr' public key + /// @param space number of bytes of memory to allocate + /// @param owner owner to use to derive the 'addr' account address + function allocate_with_seed(address addr, address base, string seed, uint64 space, address owner) internal { + AccountMeta[2] metas = [ + AccountMeta({pubkey: addr, is_signer: false, is_writable: true}), + AccountMeta({pubkey: base, is_signer: true, is_writable: false}) + ]; + + bytes bincode = new bytes(seed.length + 84); + bincode.writeUint32LE(uint32(Instruction.AllocateWithSeed), 0); + bincode.writeAddress(base, 4); + bincode.writeUint64LE(seed.length, 36); + bincode.writeString(seed, 44); + uint32 offset = 44 + seed.length; + bincode.writeUint64LE(space, offset); + offset += 8; + bincode.writeAddress(owner, offset); + + systemAddress.call{accounts: metas}(bincode); + } + + /// Create a new nonce account on Solana using a public key derived from a seed + /// + /// @param from public key for the account from which to transfer lamports to the new account + /// @param nonce the public key for the account to be created. The public key must match create_with_seed(base, seed, systemAddress) + /// @param base the base address that derived the 'nonce' key using the seed + /// @param seed the string utilized to create the 'addr' public key + /// @param authority The entity authorized to execute nonce instructions on the account + /// @param lamports amount of lamports to be transfered to the new account + function create_nonce_account_with_seed(address from, address nonce, address base, string seed, address authority, uint64 lamports) internal { + create_account_with_seed(from, nonce, base, seed, lamports, state_size, systemAddress); + + AccountMeta[3] metas = [ + AccountMeta({pubkey: nonce, is_signer: false, is_writable: true}), + AccountMeta({pubkey: recentBlockHashes, is_signer: false, is_writable: false}), + AccountMeta({pubkey: rentAddress, is_signer: false, is_writable: false}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.InitializeNonceAccount), authority); + systemAddress.call{accounts: metas}(bincode); + } + + /// Create a new account on Solana + /// + /// @param from public key for the account from which to transfer lamports to the new account + /// @param nonce the public key for the nonce account to be created + /// @param authority The entity authorized to execute nonce instructions on the account + /// @param lamports amount of lamports to be transfered to the new account + function create_nonce_account(address from, address nonce, address authority, uint64 lamports) internal { + create_account(from, nonce, lamports, state_size, systemAddress); + + AccountMeta[3] metas = [ + AccountMeta({pubkey: nonce, is_signer: false, is_writable: true}), + AccountMeta({pubkey: recentBlockHashes, is_signer: false, is_writable: false}), + AccountMeta({pubkey: rentAddress, is_signer: false, is_writable: false}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.InitializeNonceAccount), authority); + systemAddress.call{accounts: metas}(bincode); + } + + /// Consumes a stored nonce, replacing it with a successor + /// + /// @param nonce_pubkey the public key for the nonce account + /// @param authorized_pubkey the publick key for the entity authorized to execute instructins on the account + function advance_nonce_account(address nonce_pubkey, address authorized_pubkey) internal { + AccountMeta[3] metas = [ + AccountMeta({pubkey: nonce_pubkey, is_signer: false, is_writable: true}), + AccountMeta({pubkey: recentBlockHashes, is_signer: false, is_writable: false}), + AccountMeta({pubkey: authorized_pubkey, is_signer: true, is_writable: false}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.AdvanceNounceAccount)); + systemAddress.call{accounts: metas}(bincode); + } + + /// Withdraw funds from a nonce account + /// + /// @param nonce_pubkey the public key for the nonce account + /// @param authorized_pubkey the public key for the entity authorized to execute instructins on the account + /// @param to_pubkey the recipient account + /// @param lamports the number of lamports to withdraw + function withdraw_nonce_account(address nonce_pubkey, address authorized_pubkey, address to_pubkey, uint64 lamports) internal { + AccountMeta[5] metas = [ + AccountMeta({pubkey: nonce_pubkey, is_signer: false, is_writable: true}), + AccountMeta({pubkey: to_pubkey, is_signer: false, is_writable: true}), + AccountMeta({pubkey: recentBlockHashes, is_signer: false, is_writable: false}), + AccountMeta({pubkey: rentAddress, is_signer: false, is_writable: false}), + AccountMeta({pubkey: authorized_pubkey, is_signer: true, is_writable: false}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.WithdrawNonceAccount), lamports); + systemAddress.call{accounts: metas}(bincode); + } + + /// Change the entity authorized to execute nonce instructions on the account + /// + /// @param nonce_pubkey the public key for the nonce account + /// @param authorized_pubkey the public key for the entity authorized to execute instructins on the account + /// @param new_authority + function authorize_nonce_account(address nonce_pubkey, address authorized_pubkey, address new_authority) internal { + AccountMeta[2] metas = [ + AccountMeta({pubkey: nonce_pubkey, is_signer: false, is_writable: true}), + AccountMeta({pubkey: authorized_pubkey, is_signer: true, is_writable: false}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.AuthorizeNonceAccount), new_authority); + systemAddress.call{accounts: metas}(bincode); + } + + /// One-time idempotent upgrade of legacy nonce version in order to bump them out of chain domain. + /// + /// @param nonce the public key for the nonce account + // This is not available on Solana v1.9.15 + function upgrade_nonce_account(address nonce) internal { + AccountMeta[1] meta = [ + AccountMeta({pubkey: nonce, is_signer: false, is_writable: true}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.UpgradeNonceAccount)); + systemAddress.call{accounts: meta}(bincode); + } +} diff --git a/cross-chain/solana/tsconfig.json b/cross-chain/solana/tsconfig.json new file mode 100644 index 000000000..b9e8d9362 --- /dev/null +++ b/cross-chain/solana/tsconfig.json @@ -0,0 +1,63 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} \ No newline at end of file