├── .prettierignore ├── tsconfig.types.json ├── .yarn └── sdks │ ├── eslint │ ├── package.json │ └── bin │ │ └── eslint.js │ ├── prettier │ ├── package.json │ └── index.js │ ├── integrations.yml │ └── typescript │ ├── package.json │ └── bin │ ├── tsc │ └── tsserver ├── .vscode ├── extensions.json └── settings.json ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── .prettierrc ├── .yarnrc.yml ├── src ├── utils │ ├── trezor │ │ ├── index.ts │ │ ├── sign.ts │ │ └── transformations.ts │ ├── errors.ts │ ├── logger.ts │ └── common.ts ├── types │ ├── trezor.ts │ └── types.ts ├── constants │ └── index.ts ├── index.ts └── methods │ ├── randomImprove.ts │ └── largestFirst.ts ├── .github └── workflows │ └── build.yml ├── tsconfig.base.json ├── jest.config.js ├── tests ├── utils │ ├── trezor │ │ ├── transformations.test.ts │ │ ├── sign.test.ts │ │ └── fixtures │ │ │ ├── sign.ts │ │ │ └── transformations.ts │ ├── common.test.ts │ └── fixtures │ │ └── common.ts ├── methods │ ├── randomImprove.test.ts │ ├── largestFirst.test.ts │ └── fixtures │ │ ├── randomImprove.ts │ │ └── largestFirst.ts ├── setup.ts └── fixtures │ └── constants.ts ├── eslint.config.mjs ├── docs └── badge-coverage.svg ├── CHANGELOG.MD ├── package.json ├── .gitignore └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | OpenApi.ts -------------------------------------------------------------------------------- /tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.esm.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint", 3 | "version": "8.6.0-sdk", 4 | "main": "./lib/api.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prettier", 3 | "version": "2.3.2-sdk", 4 | "main": "./index.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarn/sdks/integrations.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by @yarnpkg/sdks. 2 | # Manual changes might be lost! 3 | 4 | integrations: 5 | - vscode 6 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "4.4.4-sdk", 4 | "main": "./lib/typescript.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "es2017", 5 | "module": "CommonJS", 6 | "outDir": "lib/cjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "es2017", 5 | "module": "ES2015", 6 | "outDir": "lib/esm" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "arrowParens": "avoid", 4 | "bracketSpacing": true, 5 | "singleQuote": true, 6 | "semi": true, 7 | "trailingComma": "all", 8 | "tabWidth": 2, 9 | "useTabs": false, 10 | "jsxBracketSameLine": false 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.yarn": true, 4 | "**/.pnp.*": true 5 | }, 6 | "typescript.tsdk": ".yarn/sdks/typescript/lib", 7 | "typescript.enablePromptUseWorkspaceTsdk": true, 8 | "cSpell.words": ["DEREGISTRATION", "drep", "preprod"] 9 | } 10 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | supportedArchitectures: 8 | os: 9 | - current 10 | - darwin 11 | - linux 12 | cpu: 13 | - current 14 | - x64 15 | - arm64 16 | libc: 17 | - current 18 | - glibc 19 | - musl 20 | 21 | yarnPath: .yarn/releases/yarn-4.5.0.cjs 22 | -------------------------------------------------------------------------------- /src/utils/trezor/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | transformToTokenBundle, 3 | transformToTrezorInputs, 4 | transformToTrezorOutputs, 5 | drepIdToHex, 6 | } from './transformations'; 7 | import { signTransaction } from './sign'; 8 | 9 | export { 10 | transformToTokenBundle, 11 | transformToTrezorInputs, 12 | transformToTrezorOutputs, 13 | signTransaction, 14 | drepIdToHex, 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | import { ERROR } from '../constants'; 2 | 3 | type ErrorTypeKeys = keyof typeof ERROR; 4 | type ErrorObjectType = typeof ERROR[ErrorTypeKeys]; 5 | 6 | export class CoinSelectionError extends Error { 7 | code: ErrorObjectType['code']; 8 | constructor(errorObject: ErrorObjectType) { 9 | super(errorObject.message); 10 | this.name = 'CoinSelectionError'; 11 | this.code = errorObject.code; 12 | this.message = errorObject.message; 13 | Object.setPrototypeOf(this, CoinSelectionError.prototype); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: coin-selection 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [18.x] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - run: yarn 18 | - run: yarn run lint 19 | - run: yarn run type-check 20 | - run: yarn run test 21 | - run: yarn run build 22 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require prettier/index.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real prettier/index.js your application uses 20 | module.exports = absRequire(`prettier/index.js`); 21 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "declaration": true, 5 | "isolatedModules": true, 6 | "strict": true, 7 | "noImplicitAny": true, 8 | "strictNullChecks": true, 9 | "strictFunctionTypes": true, 10 | "strictBindCallApply": true, 11 | "strictPropertyInitialization": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": false, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "esModuleInterop": true, 19 | "skipLibCheck": true, 20 | "forceConsistentCasingInFileNames": true 21 | }, 22 | "include": ["./src/**/*"] 23 | } 24 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/bin/tsc 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/bin/tsc your application uses 20 | module.exports = absRequire(`typescript/bin/tsc`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/bin/eslint.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint/bin/eslint.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint/bin/eslint.js your application uses 20 | module.exports = absRequire(`eslint/bin/eslint.js`); 21 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: '.', 3 | resetMocks: true, 4 | testEnvironment: 'node', 5 | bail: true, 6 | moduleFileExtensions: ['ts', 'js'], 7 | collectCoverage: true, 8 | coveragePathIgnorePatterns: ['/node_modules/'], 9 | testMatch: ['/tests/**/*.test.ts'], 10 | coverageReporters: ['json-summary', 'lcov', 'text', 'text-summary'], 11 | collectCoverageFrom: ['/src/**/*.ts'], 12 | setupFilesAfterEnv: ['/tests/setup.ts'], 13 | transform: { 14 | '^.+\\.(t|j)sx?$': ['@swc-node/jest'], 15 | }, 16 | coverageThreshold: { 17 | // global: { 18 | // branches: 37, 19 | // functions: 28, 20 | // lines: 40, 21 | // statements: 40, 22 | // }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsserver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/bin/tsserver 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/bin/tsserver your application uses 20 | module.exports = absRequire(`typescript/bin/tsserver`); 21 | -------------------------------------------------------------------------------- /tests/utils/trezor/transformations.test.ts: -------------------------------------------------------------------------------- 1 | import * as utils from '../../../src/utils/trezor/transformations'; 2 | import * as fixtures from './fixtures/transformations'; 3 | import { FinalOutput } from '../../../src/types/types'; 4 | 5 | describe('trezor transformation utils', () => { 6 | fixtures.transformToTrezorOutputs.forEach(f => { 7 | test(f.description, () => { 8 | expect( 9 | utils.transformToTrezorOutputs( 10 | f.outputs as FinalOutput[], 11 | f.changeAddressParameters, 12 | ), 13 | ).toMatchObject(f.result); 14 | }); 15 | }); 16 | 17 | fixtures.drepIdToHex.forEach(f => { 18 | test(f.description, () => { 19 | expect(utils.drepIdToHex(f.drepId)).toStrictEqual(f.result); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | interface Logger { 2 | debug: (...args: unknown[]) => void; 3 | } 4 | 5 | const myFormat = ({ 6 | level, 7 | args, 8 | label, 9 | timestamp, 10 | }: { 11 | level: string; 12 | args: unknown[]; 13 | label: string; 14 | timestamp: string | undefined; 15 | }): unknown[] => { 16 | if (timestamp) { 17 | return [`${timestamp} [${label}] ${level}:`, ...args]; 18 | } 19 | return [`[${label}] ${level}:`, ...args]; 20 | }; 21 | 22 | export const getLogger = (debug: boolean): Logger => { 23 | const label = '@fivebinaries/coin-selection'; 24 | return { 25 | debug: (...args) => { 26 | if (!debug) return; 27 | const formattedMessage = myFormat({ 28 | level: 'DEBUG', 29 | args: args, 30 | // timestamp: new Date().toISOString(), 31 | timestamp: undefined, 32 | label, 33 | }); 34 | 35 | console.log(...formattedMessage); 36 | }, 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /tests/utils/trezor/sign.test.ts: -------------------------------------------------------------------------------- 1 | import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs'; 2 | import * as utils from '../../../src/utils/trezor/sign'; 3 | import * as fixtures from './fixtures/sign'; 4 | 5 | describe('trezor sign utils', () => { 6 | fixtures.sign.forEach(f => { 7 | test(f.description, () => { 8 | const signedTx = utils.signTransaction(f.hex, f.witnesses, { 9 | testnet: f.testnet, 10 | }); 11 | expect( 12 | utils.signTransaction(f.hex, f.witnesses, { testnet: f.testnet }), 13 | ).toBe(f.signedTx); 14 | 15 | const tx = CardanoWasm.Transaction.from_bytes( 16 | Buffer.from(signedTx, 'hex'), 17 | ); 18 | const txhash = CardanoWasm.FixedTransaction.new_from_body_bytes( 19 | tx.body().to_bytes(), 20 | ) 21 | .transaction_hash() 22 | .to_hex(); 23 | 24 | // just sanity check, signing shouldn't change the hash 25 | expect(txhash).toBe(f.txHash); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | import tsParser from '@typescript-eslint/parser'; 6 | import tsPlugin from '@typescript-eslint/eslint-plugin'; 7 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 8 | 9 | export default tseslint.config({ 10 | files: ['**/*.ts'], // Applies to TypeScript files 11 | languageOptions: { 12 | parser: tsParser, 13 | parserOptions: { 14 | project: './tsconfig.types.json', // Ensure this path is correct 15 | tsconfigRootDir: './', // This helps in locating tsconfig.json 16 | }, 17 | }, 18 | plugins: { 19 | '@typescript-eslint': tsPlugin, 20 | prettier: eslintPluginPrettierRecommended.plugins.prettier, 21 | }, 22 | rules: { 23 | 'no-console': 'off', 24 | 'arrow-parens': [2, 'as-needed'], 25 | // 'prettier/prettier': 2, 26 | }, 27 | ignores: ['dist', 'node_modules', '.eslintrc.js', 'jest.config.js', 'lib'], 28 | }); 29 | -------------------------------------------------------------------------------- /docs/badge-coverage.svg: -------------------------------------------------------------------------------- 1 | Coverage: 87%Coverage87% -------------------------------------------------------------------------------- /CHANGELOG.MD: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## Unreleased 9 | 10 | ## [3.0.0] - 2024-12-09 11 | 12 | ### Added 13 | 14 | - Support for voting delegation certificates 15 | 16 | ### Changed 17 | 18 | - Upgraded cardano-serialization-lib to Chang-compatible release (v13) 19 | - All set types are now serialized with a tag "258", which will become mandatory after the next hard fork (HF) 20 | 21 | ## [2.2.1] - 2023-11-29 22 | 23 | ### Fixed 24 | 25 | - UTXO_BALANCE_INSUFFICIENT while using `setMax` on ADA output 26 | 27 | ## [2.2.0] - 2023-10-06 28 | 29 | ### Added 30 | 31 | - nodejs compatibility 32 | 33 | ## [2.1.0] - 2022-11-22 34 | 35 | ### Fixed 36 | 37 | - occasionally too low fee while using `setMax` on token output 38 | - order of inputs returned in txPlan did not always match the order of inputs in constructed transaction CBOR 39 | 40 | ### Changed 41 | 42 | - Upgraded deps, cardano-serialization-lib to babbage-compatible release (v11) 43 | 44 | ## [2.0.0] - 2022-02-14 45 | 46 | ### Added 47 | 48 | - Initial release 49 | -------------------------------------------------------------------------------- /src/types/trezor.ts: -------------------------------------------------------------------------------- 1 | import { CardanoAddressType } from './types'; 2 | 3 | export interface CardanoInput { 4 | path: string | number[]; 5 | prev_hash: string; 6 | prev_index: number; 7 | } 8 | 9 | export type CardanoToken = { 10 | assetNameBytes: string; 11 | amount: string; 12 | }; 13 | 14 | export type CardanoAssetGroup = { 15 | policyId: string; 16 | tokenAmounts: CardanoToken[]; 17 | }; 18 | 19 | export interface CardanoCertificatePointer { 20 | blockIndex: number; 21 | txIndex: number; 22 | certificateIndex: number; 23 | } 24 | 25 | export interface CardanoAddressParameters { 26 | addressType: CardanoAddressType; 27 | path: string | number[]; 28 | stakingPath?: string | number[]; 29 | stakingKeyHash?: string; 30 | certificatePointer?: CardanoCertificatePointer; 31 | } 32 | 33 | export type CardanoOutput = 34 | | { 35 | addressParameters: CardanoAddressParameters; 36 | amount: string; 37 | tokenBundle?: CardanoAssetGroup[]; 38 | } 39 | | { 40 | address: string; 41 | amount: string; 42 | tokenBundle?: CardanoAssetGroup[]; 43 | }; 44 | 45 | export enum CardanoTxWitnessType { 46 | BYRON_WITNESS = 0, 47 | SHELLEY_WITNESS = 1, 48 | } 49 | 50 | export type CardanoSignedTxWitness = { 51 | type: CardanoTxWitnessType; 52 | pubKey: string; 53 | signature: string; 54 | chainCode?: string | null; 55 | }; 56 | -------------------------------------------------------------------------------- /tests/methods/randomImprove.test.ts: -------------------------------------------------------------------------------- 1 | import * as fixtures from './fixtures/randomImprove'; 2 | import { randomImprove } from '../../src/methods/randomImprove'; 3 | import { sanityCheck } from '../setup'; 4 | 5 | describe('coinSelection - randomImprove', () => { 6 | fixtures.nonFinalCompose.forEach(f => { 7 | const { utxos, outputs, changeAddress } = f; 8 | 9 | test(f.description, () => { 10 | const res = randomImprove( 11 | { 12 | utxos, 13 | outputs, 14 | changeAddress, 15 | }, 16 | f.options, 17 | ); 18 | expect(res).toMatchObject(f.result); 19 | }); 20 | }); 21 | 22 | fixtures.coinSelection.forEach(f => { 23 | const { utxos, outputs, changeAddress, ttl } = f; 24 | 25 | test(f.description, () => { 26 | const res = randomImprove( 27 | { 28 | utxos, 29 | outputs, 30 | changeAddress, 31 | ttl, 32 | }, 33 | f.options, 34 | ); 35 | expect(res).toMatchObject(f.result); 36 | sanityCheck(res); 37 | }); 38 | }); 39 | 40 | fixtures.exceptions.forEach(f => { 41 | const { utxos, outputs, changeAddress } = f; 42 | test(f.description, () => { 43 | const res = () => 44 | randomImprove( 45 | { 46 | utxos, 47 | outputs, 48 | changeAddress, 49 | }, 50 | f.options, 51 | ); 52 | expect(res).toThrowError(expect.objectContaining({ code: f.result })); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fivebinaries/coin-selection", 3 | "version": "3.0.0", 4 | "description": "", 5 | "keywords": [ 6 | "coin selection" 7 | ], 8 | "license": "Apache-2.0", 9 | "author": "fivebinaries.com", 10 | "main": "lib/cjs/index.js", 11 | "module": "lib/esm/index.js", 12 | "browser": { 13 | "@emurgo/cardano-serialization-lib-nodejs": "@emurgo/cardano-serialization-lib-browser" 14 | }, 15 | "files": [ 16 | "lib/**/*.js", 17 | "lib/**/*.ts" 18 | ], 19 | "scripts": { 20 | "build": "yarn clean && tsc -p tsconfig.esm.json && tsc -p tsconfig.cjs.json", 21 | "clean": "rimraf lib", 22 | "lint": "eslint ./src/**/*.ts", 23 | "prepublishOnly": "yarn build", 24 | "type-check": "yarn tsc -p tsconfig.types.json", 25 | "test": "yarn run-s 'test:*'", 26 | "test:unit": "jest -c ./jest.config.js", 27 | "test:badges": "make-coverage-badge --output-path ./docs/badge-coverage.svg" 28 | }, 29 | "devDependencies": { 30 | "@eslint/js": "^9.11.1", 31 | "@swc-node/jest": "^1.8.12", 32 | "@swc/core": "1.7.26", 33 | "@swc/helpers": "^0.5.13", 34 | "@types/jest": "^29.5.13", 35 | "@types/node": "^16.3.2", 36 | "@typescript-eslint/eslint-plugin": "^8.8.0", 37 | "@typescript-eslint/parser": "8.8.0", 38 | "eslint": "^9.11.1", 39 | "eslint-config-prettier": "^9.1.0", 40 | "eslint-plugin-import": "^2.30.0", 41 | "eslint-plugin-prettier": "^5.2.1", 42 | "jest": "^29.7.0", 43 | "make-coverage-badge": "^1.2.0", 44 | "npm-run-all": "^4.1.5", 45 | "prettier": "3.3.3", 46 | "rimraf": "^6.0.1", 47 | "typescript": "^5.6.2", 48 | "typescript-eslint": "^8.8.0" 49 | }, 50 | "dependencies": { 51 | "@emurgo/cardano-serialization-lib-browser": "^13.2.0", 52 | "@emurgo/cardano-serialization-lib-nodejs": "13.2.0" 53 | }, 54 | "packageManager": "yarn@4.5.0" 55 | } 56 | -------------------------------------------------------------------------------- /tests/utils/common.test.ts: -------------------------------------------------------------------------------- 1 | import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs'; 2 | import * as utils from '../../src/utils/common'; 3 | import * as fixtures from './fixtures/common'; 4 | 5 | describe('common utils', () => { 6 | test('multiAssetToArray', () => { 7 | const multiAsset = utils.buildMultiAsset([ 8 | { 9 | quantity: '1000', 10 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 11 | }, 12 | ]); 13 | const res = utils.multiAssetToArray(multiAsset); 14 | expect(res).toMatchObject([ 15 | { 16 | quantity: '1000', 17 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 18 | }, 19 | ]); 20 | }); 21 | 22 | fixtures.filterUtxos.forEach(f => { 23 | test(f.description, () => { 24 | expect(utils.filterUtxos(f.utxos, f.asset)).toMatchObject(f.result); 25 | }); 26 | }); 27 | 28 | fixtures.buildTxOutput.forEach(f => { 29 | test(f.description, () => { 30 | const output = utils.buildTxOutput(f.output, f.dummyAddress); 31 | const assets = utils.multiAssetToArray(output.amount().multiasset()); 32 | 33 | let address; 34 | if (CardanoWasm.ByronAddress.is_valid(f.result.address)) { 35 | // expecting byron address 36 | address = CardanoWasm.ByronAddress.from_bytes( 37 | output.address().to_bytes(), 38 | ).to_base58(); 39 | } else { 40 | address = output.address().to_bech32(); // by default expect shelley 41 | } 42 | expect(output.amount().coin().to_str()).toBe(f.result.amount); 43 | expect(address).toBe(f.result.address); 44 | expect(assets).toStrictEqual(f.result.assets); 45 | }); 46 | }); 47 | 48 | fixtures.orderInputs.forEach(f => { 49 | test(f.description, () => { 50 | const inputs = utils.orderInputs( 51 | f.inputsToOrder, 52 | CardanoWasm.TransactionBody.from_bytes(Buffer.from(f.txBodyHex, 'hex')), 53 | ); 54 | expect(inputs).toStrictEqual(f.result); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs'; 2 | 3 | export const CertificateType = { 4 | STAKE_REGISTRATION: 0, 5 | STAKE_DEREGISTRATION: 1, 6 | STAKE_DELEGATION: 2, 7 | STAKE_POOL_REGISTRATION: 3, 8 | STAKE_REGISTRATION_CONWAY: 7, 9 | STAKE_DEREGISTRATION_CONWAY: 8, 10 | VOTE_DELEGATION: 9, 11 | } as const; 12 | 13 | export const ERROR = { 14 | UTXO_BALANCE_INSUFFICIENT: { 15 | code: 'UTXO_BALANCE_INSUFFICIENT', 16 | message: 'UTxO balance insufficient', 17 | }, 18 | UTXO_VALUE_TOO_SMALL: { 19 | code: 'UTXO_VALUE_TOO_SMALL', 20 | message: 'UTxO value too small', 21 | }, 22 | UNSUPPORTED_CERTIFICATE_TYPE: { 23 | code: 'UNSUPPORTED_CERTIFICATE_TYPE', 24 | message: 'Unsupported certificate type', 25 | }, 26 | UTXO_NOT_FRAGMENTED_ENOUGH: { 27 | code: 'UTXO_NOT_FRAGMENTED_ENOUGH', 28 | message: 'UTxO Not fragmented enough.', 29 | }, 30 | } as const; 31 | 32 | export const CARDANO_PARAMS = { 33 | PROTOCOL_MAGICS: { 34 | mainnet: CardanoWasm.NetworkInfo.mainnet().protocol_magic(), 35 | testnet_preprod: CardanoWasm.NetworkInfo.testnet_preprod().protocol_magic(), 36 | testnet_preview: CardanoWasm.NetworkInfo.testnet_preview().protocol_magic(), 37 | }, 38 | NETWORK_IDS: { 39 | mainnet: CardanoWasm.NetworkInfo.mainnet().network_id(), 40 | testnet_preprod: CardanoWasm.NetworkInfo.testnet_preprod().network_id(), 41 | testnet_preview: CardanoWasm.NetworkInfo.testnet_preview().network_id(), 42 | }, 43 | COINS_PER_UTXO_BYTE: '4310', 44 | MAX_TX_SIZE: 16384, 45 | MAX_VALUE_SIZE: 5000, 46 | } as const; 47 | 48 | // https://github.com/vacuumlabs/adalite/blob/d8ba3bb1ff439ae8e02abd99163435a989d97961/app/frontend/wallet/shelley/transaction/constants.ts 49 | // policyId is 28 bytes, assetName max 32 bytes, together with quantity makes 50 | // max token size about 70 bytes, max output size is 4000 => 4000 / 70 ~ 50 51 | export const MAX_TOKENS_PER_OUTPUT = 50; 52 | 53 | export const DATA_COST_PER_UTXO_BYTE = CardanoWasm.DataCost.new_coins_per_byte( 54 | CardanoWasm.BigNum.from_str(CARDANO_PARAMS.COINS_PER_UTXO_BYTE), 55 | ); 56 | -------------------------------------------------------------------------------- /tests/methods/largestFirst.test.ts: -------------------------------------------------------------------------------- 1 | import * as fixtures from './fixtures/largestFirst'; 2 | import { largestFirst } from '../../src/methods/largestFirst'; 3 | import { sanityCheck } from '../setup'; 4 | 5 | describe('coinSelection - largestFirst', () => { 6 | fixtures.nonFinalCompose.forEach(f => { 7 | test(f.description, () => { 8 | const { 9 | utxos, 10 | outputs, 11 | changeAddress, 12 | certificates, 13 | withdrawals, 14 | accountPubKey, 15 | } = f; 16 | const res = largestFirst( 17 | { 18 | utxos, 19 | outputs, 20 | changeAddress, 21 | certificates, 22 | withdrawals, 23 | accountPubKey, 24 | }, 25 | f.options, 26 | ); 27 | 28 | expect(res).toMatchObject(f.result); 29 | }); 30 | }); 31 | 32 | fixtures.coinSelection.forEach(f => { 33 | test(f.description, () => { 34 | const { 35 | utxos, 36 | outputs, 37 | changeAddress, 38 | certificates, 39 | withdrawals, 40 | accountPubKey, 41 | ttl, 42 | } = f; 43 | const res = largestFirst( 44 | { 45 | utxos, 46 | outputs, 47 | changeAddress, 48 | certificates, 49 | withdrawals, 50 | accountPubKey, 51 | ttl, 52 | }, 53 | f.options, 54 | ); 55 | 56 | expect(res).toMatchObject(f.result); 57 | sanityCheck(res); 58 | }); 59 | }); 60 | 61 | fixtures.exceptions.forEach(f => { 62 | test(f.description, () => { 63 | const { 64 | utxos, 65 | outputs, 66 | changeAddress, 67 | certificates, 68 | withdrawals, 69 | accountPubKey, 70 | } = f; 71 | const res = () => 72 | largestFirst( 73 | { 74 | utxos, 75 | outputs, 76 | changeAddress, 77 | certificates, 78 | withdrawals, 79 | accountPubKey, 80 | }, 81 | f.options, 82 | ); 83 | 84 | expect(res).toThrowError(expect.objectContaining({ code: f.result })); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | jest.setTimeout(30000); 2 | 3 | import { 4 | BigNum, 5 | TransactionBody, 6 | } from '@emurgo/cardano-serialization-lib-nodejs'; 7 | import { CoinSelectionResult } from '../src/types/types'; 8 | import { multiAssetToArray } from '../src/utils/common'; 9 | 10 | export const sanityCheck = (res: CoinSelectionResult): void => { 11 | const totalAdaInputs = res.inputs 12 | .reduce( 13 | (acc, input) => 14 | acc.checked_add( 15 | BigNum.from_str( 16 | input.amount.find(a => a.unit === 'lovelace')?.quantity || '0', 17 | ), 18 | ), 19 | BigNum.from_str('0'), 20 | ) 21 | .checked_add(BigNum.from_str(res.withdrawal)); 22 | 23 | let totalAdaOutputs = res.outputs.reduce( 24 | (acc, output) => acc.checked_add(BigNum.from_str(output.amount ?? '0')), 25 | BigNum.from_str('0'), 26 | ); 27 | if (res.deposit.startsWith('-')) { 28 | totalAdaOutputs = totalAdaOutputs.clamped_sub( 29 | BigNum.from_str(res.deposit.slice(1)), 30 | ); 31 | } else { 32 | totalAdaOutputs = totalAdaOutputs.checked_add(BigNum.from_str(res.deposit)); 33 | } 34 | 35 | // Check ADA value: inputs + withdrawals (rewards) = outputs + fee 36 | const delta = totalAdaInputs.compare( 37 | totalAdaOutputs.checked_add(BigNum.from_str(res.fee)), 38 | ); 39 | 40 | expect(delta).toBe(0); 41 | 42 | // txBody sanity check 43 | const tx = TransactionBody.from_bytes(Buffer.from(res.tx.body, 'hex')); 44 | expect(tx.inputs().len()).toBe(res.inputs.length); 45 | expect(tx.outputs().len()).toBe(res.outputs.length); 46 | 47 | for (let i = 0; i < res.outputs.length; i++) { 48 | // lovelace amount 49 | expect(tx.outputs().get(i).amount().coin().to_str()).toBe( 50 | res.outputs[i].amount, 51 | ); 52 | // address 53 | expect(tx.outputs().get(i).address().to_bech32()).toBe( 54 | res.outputs[i].address, 55 | ); 56 | // assets 57 | expect( 58 | multiAssetToArray(tx.outputs().get(i).amount().multiasset()), 59 | ).toMatchObject(res.outputs[i].assets); 60 | } 61 | 62 | // fee set in txBuilder really matches fee returned in json object 63 | expect(res.fee).toBe(tx.fee().to_str()); 64 | }; 65 | -------------------------------------------------------------------------------- /src/utils/trezor/sign.ts: -------------------------------------------------------------------------------- 1 | import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs'; 2 | import { 3 | CardanoSignedTxWitness, 4 | CardanoTxWitnessType, 5 | } from '../../types/trezor'; 6 | import { getProtocolMagic } from '../common'; 7 | 8 | export const signTransaction = ( 9 | txBodyHex: string, 10 | // txMetadata: CardanoWasm.AuxiliaryData, 11 | signedWitnesses: CardanoSignedTxWitness[], 12 | options?: { testnet?: boolean }, 13 | ): string => { 14 | const txBody = CardanoWasm.TransactionBody.from_bytes( 15 | Uint8Array.from(Buffer.from(txBodyHex, 'hex')), 16 | ); 17 | const witnesses = CardanoWasm.TransactionWitnessSet.new(); 18 | const vkeyWitnesses = CardanoWasm.Vkeywitnesses.new(); 19 | const bootstrapWitnesses = CardanoWasm.BootstrapWitnesses.new(); 20 | 21 | signedWitnesses.forEach(w => { 22 | const vKey = CardanoWasm.Vkey.new( 23 | CardanoWasm.PublicKey.from_bytes(Buffer.from(w.pubKey, 'hex')), 24 | ); 25 | const signature = CardanoWasm.Ed25519Signature.from_bytes( 26 | Buffer.from(w.signature, 'hex'), 27 | ); 28 | 29 | if (w.type === CardanoTxWitnessType.SHELLEY_WITNESS) { 30 | // Shelley witness 31 | const vKeyWitness = CardanoWasm.Vkeywitness.new(vKey, signature); 32 | vkeyWitnesses.add(vKeyWitness); 33 | } else if (w.type === CardanoTxWitnessType.BYRON_WITNESS) { 34 | // Byron witness (TODO: not used, needs testing) 35 | if (w.chainCode) { 36 | const xpubHex = `${w.pubKey}${w.chainCode}`; 37 | const bip32Key = CardanoWasm.Bip32PublicKey.from_bytes( 38 | Buffer.from(xpubHex, 'hex'), 39 | ); 40 | const byronAddress = CardanoWasm.ByronAddress.icarus_from_key( 41 | bip32Key, 42 | getProtocolMagic(!!options?.testnet), 43 | ); 44 | const bootstrapWitness = CardanoWasm.BootstrapWitness.new( 45 | vKey, 46 | signature, 47 | Buffer.from(w.chainCode, 'hex'), 48 | byronAddress.attributes(), 49 | ); 50 | bootstrapWitnesses.add(bootstrapWitness); 51 | } 52 | } 53 | }); 54 | 55 | if (bootstrapWitnesses.len() > 0) { 56 | witnesses.set_bootstraps(bootstrapWitnesses); 57 | } 58 | if (vkeyWitnesses.len() > 0) { 59 | witnesses.set_vkeys(vkeyWitnesses); 60 | } 61 | 62 | const transaction = CardanoWasm.Transaction.new(txBody, witnesses); 63 | const serializedTx = Buffer.from(transaction.to_bytes()).toString('hex'); 64 | return serializedTx; 65 | }; 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS ### 2 | # General 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Icon must end with two \r 8 | Icon 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear in the root of a volume 14 | .DocumentRevisions-V100 15 | .fseventsd 16 | .Spotlight-V100 17 | .TemporaryItems 18 | .Trashes 19 | .VolumeIcon.icns 20 | .com.apple.timemachine.donotpresent 21 | 22 | # Directories potentially created on remote AFP share 23 | .AppleDB 24 | .AppleDesktop 25 | Network Trash Folder 26 | Temporary Items 27 | .apdisk 28 | 29 | # Logs 30 | logs 31 | *.log 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | lerna-debug.log* 36 | .pnpm-debug.log* 37 | 38 | # Diagnostic reports (https://nodejs.org/api/report.html) 39 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 40 | 41 | # Runtime data 42 | pids 43 | *.pid 44 | *.seed 45 | *.pid.lock 46 | 47 | # Directory for instrumented libs generated by jscoverage/JSCover 48 | lib-cov 49 | 50 | # Coverage directory used by tools like istanbul 51 | coverage 52 | *.lcov 53 | 54 | # nyc test coverage 55 | .nyc_output 56 | 57 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 58 | .grunt 59 | 60 | # Bower dependency directory (https://bower.io/) 61 | bower_components 62 | 63 | # node-waf configuration 64 | .lock-wscript 65 | 66 | # Compiled binary addons (https://nodejs.org/api/addons.html) 67 | build/Release 68 | 69 | # Dependency directories 70 | node_modules/ 71 | jspm_packages/ 72 | 73 | # Snowpack dependency directory (https://snowpack.dev/) 74 | web_modules/ 75 | 76 | # TypeScript cache 77 | *.tsbuildinfo 78 | 79 | # Optional npm cache directory 80 | .npm 81 | 82 | # Optional eslint cache 83 | .eslintcache 84 | 85 | # Microbundle cache 86 | .rpt2_cache/ 87 | .rts2_cache_cjs/ 88 | .rts2_cache_es/ 89 | .rts2_cache_umd/ 90 | 91 | # Optional REPL history 92 | .node_repl_history 93 | 94 | # Output of 'npm pack' 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | .yarn-integrity 99 | 100 | # dotenv environment variables file 101 | .env 102 | .env.test 103 | .env.production 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | .cache 107 | .parcel-cache 108 | 109 | # Next.js build output 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | .nuxt 115 | dist 116 | 117 | # Gatsby files 118 | .cache/ 119 | # Comment in the public line in if your project uses Gatsby and not Next.js 120 | # https://nextjs.org/blog/next-9-1#public-directory-support 121 | # public 122 | 123 | # vuepress build output 124 | .vuepress/dist 125 | 126 | # Serverless directories 127 | .serverless/ 128 | 129 | # FuseBox cache 130 | .fusebox/ 131 | 132 | # DynamoDB Local files 133 | .dynamodb/ 134 | 135 | # TernJS port file 136 | .tern-port 137 | 138 | # Stores VSCode versions used for testing VSCode extensions 139 | .vscode-test 140 | 141 | lib 142 | dist 143 | 144 | .pnp.* 145 | .yarn/* 146 | !.yarn/patches 147 | !.yarn/plugins 148 | !.yarn/releases 149 | !.yarn/sdks 150 | !.yarn/versions 151 | 152 | -------------------------------------------------------------------------------- /tests/utils/trezor/fixtures/sign.ts: -------------------------------------------------------------------------------- 1 | export const sign = [ 2 | { 3 | description: '3 inputs, 3 witnesses', 4 | hex: 'a4008382582018cc7d96f92d506dd112d101c8e33a80d0258f6b7b1aabd676e0a4ee6af08943008258201e8ba20994b708f668784eadc5a583ef090998545ebd0b357cca1098bf4112ce00825820c1654e963a73c12eea189a521fa994c8ef2d12efcfbceceb5e0e80a48988a3f601018282583900b52332e05067c86346bbf650afc120e443114358606f7e70c45542bfd7bd25149cb78c6085ca07e881870ca2dff95287e33f9edefd9b1f0d1a0016e36082583900a99f2c10c266cf5cf289f32b6b429cded9b94fe037dd0bcde73e408ca271492bbb4cf84ad488cf85b84c3b960d5ca912e93cd70cac2ae60b821a00203c89a1581c2bd1de9a0ede8302f7b860792d4fcfa9a34cafa6d0cc54d20e5b5374a1445345414c01021a0002c749031a03e2a117', 5 | testnet: true, 6 | witnesses: [ 7 | { 8 | type: 1, 9 | pubKey: 10 | '687d090fa9ea2ca54ab7a881202031c02a78595bd023e7e145d174e4aeb35d36', 11 | signature: 12 | '456b0c26401f2a0a55c5273a9fd1380074a4e231c6ff531c1dccbce886632ba3d9d331f67935182563c57496b89ff1251995efd23b8506f1bed194210472d400', 13 | chainCode: null, 14 | }, 15 | { 16 | type: 1, 17 | pubKey: 18 | 'b236a3556ad2a2be78339cd520c8c784d1b3e54fc77935f4e96183eeddf701ab', 19 | signature: 20 | 'ee4a17f8b2d37d4e1edc309c9c44b08edc94920fac36fe675f88245153948533dc6d8c24744cdd6327027477649aa06e3824c0dc3aa6cbcb1246440c29a28600', 21 | chainCode: null, 22 | }, 23 | { 24 | type: 1, 25 | pubKey: 26 | '05d3360b3af91ec66ff5ab77b9f40f568b1f5a24fd7350019f5a50a67c883e64', 27 | signature: 28 | 'a943f85e313cc907f1bc1d31b0c2cc0160c44232eb6b2cda44568cc690821d6a02a0632e864d4ef26828efd28719cb4dd63e9400dc0c0e6d7a8dccc7a414db0e', 29 | chainCode: null, 30 | }, 31 | ], 32 | signedTx: 33 | '84a400d901028382582018cc7d96f92d506dd112d101c8e33a80d0258f6b7b1aabd676e0a4ee6af08943008258201e8ba20994b708f668784eadc5a583ef090998545ebd0b357cca1098bf4112ce00825820c1654e963a73c12eea189a521fa994c8ef2d12efcfbceceb5e0e80a48988a3f601018282583900b52332e05067c86346bbf650afc120e443114358606f7e70c45542bfd7bd25149cb78c6085ca07e881870ca2dff95287e33f9edefd9b1f0d1a0016e36082583900a99f2c10c266cf5cf289f32b6b429cded9b94fe037dd0bcde73e408ca271492bbb4cf84ad488cf85b84c3b960d5ca912e93cd70cac2ae60b821a00203c89a1581c2bd1de9a0ede8302f7b860792d4fcfa9a34cafa6d0cc54d20e5b5374a1445345414c01021a0002c749031a03e2a117a100d9010283825820687d090fa9ea2ca54ab7a881202031c02a78595bd023e7e145d174e4aeb35d365840456b0c26401f2a0a55c5273a9fd1380074a4e231c6ff531c1dccbce886632ba3d9d331f67935182563c57496b89ff1251995efd23b8506f1bed194210472d400825820b236a3556ad2a2be78339cd520c8c784d1b3e54fc77935f4e96183eeddf701ab5840ee4a17f8b2d37d4e1edc309c9c44b08edc94920fac36fe675f88245153948533dc6d8c24744cdd6327027477649aa06e3824c0dc3aa6cbcb1246440c29a2860082582005d3360b3af91ec66ff5ab77b9f40f568b1f5a24fd7350019f5a50a67c883e645840a943f85e313cc907f1bc1d31b0c2cc0160c44232eb6b2cda44568cc690821d6a02a0632e864d4ef26828efd28719cb4dd63e9400dc0c0e6d7a8dccc7a414db0ef5f6', 34 | txHash: '0690e4c93d5f5f2c3379f113bdca1bc26db8364f422549f99f94ae3a597c716b', 35 | }, 36 | ]; 37 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ERROR } from './constants'; 2 | import { largestFirst } from './methods/largestFirst'; 3 | import { randomImprove } from './methods/randomImprove'; 4 | import { CoinSelectionError } from './utils/errors'; 5 | import { getLogger } from './utils/logger'; 6 | import { 7 | CoinSelectionParams, 8 | CoinSelectionResult, 9 | FinalOutput, 10 | Options, 11 | PrecomposedTransaction, 12 | } from './types/types'; 13 | import { bigNumFromStr } from './utils/common'; 14 | 15 | export const coinSelection = ( 16 | params: CoinSelectionParams, 17 | options?: Options, 18 | ): PrecomposedTransaction => { 19 | const logger = getLogger(!!options?.debug); 20 | logger.debug('Args:', { 21 | params, 22 | options, 23 | }); 24 | 25 | if (params.utxos.length === 0) { 26 | logger.debug('Empty Utxo set'); 27 | throw new CoinSelectionError(ERROR.UTXO_BALANCE_INSUFFICIENT); 28 | } 29 | 30 | const t1 = new Date().getTime(); 31 | let res: CoinSelectionResult; 32 | if ( 33 | params.outputs.find(o => o.setMax) || 34 | params.certificates.length > 0 || 35 | params.withdrawals.length > 0 || 36 | options?.forceLargestFirstSelection 37 | ) { 38 | logger.debug('Running largest-first alg'); 39 | res = largestFirst(params, options); 40 | } else { 41 | logger.debug('Running random-improve alg'); 42 | try { 43 | res = randomImprove(params, options); 44 | } catch (error) { 45 | if ( 46 | error instanceof CoinSelectionError && 47 | error.code === 'UTXO_NOT_FRAGMENTED_ENOUGH' 48 | ) { 49 | logger.debug( 50 | `random-improve failed with ${error.code}. Retrying with largest-first alg.`, 51 | ); 52 | res = largestFirst(params, options); 53 | } else { 54 | throw error; 55 | } 56 | } 57 | } 58 | 59 | const t2 = new Date().getTime(); 60 | logger.debug(`Duration: ${(t2 - t1) / 1000} seconds`); 61 | 62 | const incompleteOutputs = res.outputs.find( 63 | o => 64 | !o.address || 65 | !o.amount || 66 | // assets set with quantity = 0 67 | (o.assets.length > 0 && 68 | o.assets.find( 69 | a => 70 | !a.quantity || 71 | bigNumFromStr(a.quantity).compare(bigNumFromStr('0')) === 0, 72 | )), 73 | ); 74 | 75 | if (incompleteOutputs) { 76 | const selection = { 77 | type: 'nonfinal', 78 | fee: res.fee, 79 | totalSpent: res.totalSpent, 80 | deposit: res.deposit, 81 | withdrawal: res.withdrawal, 82 | max: res.max, 83 | } as const; 84 | logger.debug('Coin selection for a draft transaction:', selection); 85 | return selection; 86 | } else { 87 | const selection = { 88 | type: 'final', 89 | ...res, 90 | outputs: res.outputs as FinalOutput[], 91 | } as const; 92 | logger.debug('Coin selection for a final transaction:', selection); 93 | return selection; 94 | } 95 | }; 96 | 97 | export * as trezorUtils from './utils/trezor'; 98 | export * as types from './types/types'; 99 | export { CoinSelectionError } from './utils/errors'; 100 | -------------------------------------------------------------------------------- /tests/utils/trezor/fixtures/transformations.ts: -------------------------------------------------------------------------------- 1 | export const transformToTrezorOutputs = [ 2 | { 3 | description: 'Transform outputs to trezor-connect compatible output', 4 | outputs: [ 5 | { 6 | address: 7 | 'addr_test1qr23dayvk6h0r3p207qakepgqp6g7v0a5a08d8dee4rx42dprlgc2l6yjncuuym9peve74vktfqzy72jnlmkveqe2qesuc7wjg', 8 | amount: '1344798', 9 | assets: [ 10 | { 11 | unit: '21c3e7f6f954e606fe90017628b048a0067b561a4f6e2aa0e1aa613156616375756d73', 12 | quantity: '10', 13 | }, 14 | ], 15 | setMax: false, 16 | }, 17 | { 18 | address: 19 | 'addr_test1qr23dayvk6h0r3p207qakepgqp6g7v0a5a08d8dee4rx42dprlgc2l6yjncuuym9peve74vktfqzy72jnlmkveqe2qesuc7wjg', 20 | amount: '1000000', 21 | assets: [], 22 | setMax: false, 23 | }, 24 | { 25 | isChange: true, 26 | amount: '9237484', 27 | address: 28 | 'addr_test1qrm0fuq450ym8qtnln8wxzg4xgx2wftqh025rdw3t3smd4qle6svh9nacvm632nmcy6fnw9sq85tqkvhagfrhkj9tf6sjyf2vj', 29 | assets: [ 30 | { 31 | quantity: '999919', 32 | unit: '21c3e7f6f954e606fe90017628b048a0067b561a4f6e2aa0e1aa613156616375756d73', 33 | }, 34 | { 35 | quantity: '1', 36 | unit: '6b8d07d69639e9413dd637a1a815a7323c69c86abbafb66dbfdb1aa7', 37 | }, 38 | ], 39 | }, 40 | ], 41 | changeAddressParameters: { 42 | path: "m/1852'/1815'/1'/1/0", 43 | addressType: 0, // base (shelley+) 44 | stakingPath: "m/1852'/1815'/2/0", 45 | }, 46 | result: [ 47 | { 48 | address: 49 | 'addr_test1qr23dayvk6h0r3p207qakepgqp6g7v0a5a08d8dee4rx42dprlgc2l6yjncuuym9peve74vktfqzy72jnlmkveqe2qesuc7wjg', 50 | amount: '1344798', 51 | tokenBundle: [ 52 | { 53 | policyId: 54 | '21c3e7f6f954e606fe90017628b048a0067b561a4f6e2aa0e1aa6131', 55 | tokenAmounts: [{ amount: '10', assetNameBytes: '56616375756d73' }], 56 | }, 57 | ], 58 | }, 59 | { 60 | address: 61 | 'addr_test1qr23dayvk6h0r3p207qakepgqp6g7v0a5a08d8dee4rx42dprlgc2l6yjncuuym9peve74vktfqzy72jnlmkveqe2qesuc7wjg', 62 | amount: '1000000', 63 | tokenBundle: undefined, 64 | }, 65 | { 66 | addressParameters: { 67 | addressType: 0, 68 | path: "m/1852'/1815'/1'/1/0", 69 | stakingPath: "m/1852'/1815'/2/0", 70 | }, 71 | amount: '9237484', 72 | tokenBundle: [ 73 | { 74 | policyId: 75 | '21c3e7f6f954e606fe90017628b048a0067b561a4f6e2aa0e1aa6131', 76 | tokenAmounts: [ 77 | { amount: '999919', assetNameBytes: '56616375756d73' }, 78 | ], 79 | }, 80 | { 81 | policyId: 82 | '6b8d07d69639e9413dd637a1a815a7323c69c86abbafb66dbfdb1aa7', 83 | tokenAmounts: [{ amount: '1', assetNameBytes: '' }], 84 | }, 85 | ], 86 | }, 87 | ], 88 | }, 89 | ]; 90 | 91 | export const drepIdToHex = [ 92 | { 93 | description: 'keyHash drep', 94 | drepId: 'drep16pxnn38ykshfahwmkaqmke3kdqaksg4w935d7uztvh8y5l48pxv', 95 | result: { 96 | type: 0, 97 | hex: 'd04d39c4e4b42e9edddbb741bb6636683b6822ae2c68df704b65ce4a', 98 | }, 99 | }, 100 | { 101 | description: 'scriptHash drep', 102 | drepId: 'drep_script16pxnn38ykshfahwmkaqmke3kdqaksg4w935d7uztvh8y5sh6f6d', 103 | result: { 104 | type: 1, 105 | hex: 'd04d39c4e4b42e9edddbb741bb6636683b6822ae2c68df704b65ce4a', 106 | }, 107 | }, 108 | ]; 109 | -------------------------------------------------------------------------------- /src/utils/trezor/transformations.ts: -------------------------------------------------------------------------------- 1 | import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs'; 2 | 3 | import { 4 | CardanoAddressParameters, 5 | CardanoInput, 6 | CardanoOutput, 7 | } from '../../types/trezor'; 8 | import { Asset, CardanoDRepType, FinalOutput, Utxo } from '../../types/types'; 9 | import { parseAsset } from '../common'; 10 | 11 | interface AssetInPolicy { 12 | assetNameBytes: string; 13 | amount: string; 14 | } 15 | export const transformToTokenBundle = (assets: Asset[]) => { 16 | // prepare token bundle used in trezor output 17 | if (assets.length === 0) return undefined; 18 | 19 | const uniquePolicies: string[] = []; 20 | assets.forEach(asset => { 21 | const { policyId } = parseAsset(asset.unit); 22 | if (!uniquePolicies.includes(policyId)) { 23 | uniquePolicies.push(policyId); 24 | } 25 | }); 26 | 27 | const assetsByPolicy: { 28 | policyId: string; 29 | tokenAmounts: AssetInPolicy[]; 30 | }[] = []; 31 | uniquePolicies.forEach(policyId => { 32 | const assetsInPolicy: AssetInPolicy[] = []; 33 | assets.forEach(asset => { 34 | const assetInfo = parseAsset(asset.unit); 35 | if (assetInfo.policyId !== policyId) return; 36 | 37 | assetsInPolicy.push({ 38 | assetNameBytes: assetInfo.assetNameInHex, 39 | amount: asset.quantity, 40 | }); 41 | }), 42 | assetsByPolicy.push({ 43 | policyId, 44 | tokenAmounts: assetsInPolicy, 45 | }); 46 | }); 47 | 48 | return assetsByPolicy; 49 | }; 50 | 51 | export const transformToTrezorInputs = ( 52 | utxos: Utxo[], 53 | trezorUtxos: { txid: string; vout: number; path: string }[], 54 | ): CardanoInput[] => { 55 | return utxos.map(utxo => { 56 | const utxoWithPath = trezorUtxos.find( 57 | u => u.txid === utxo.txHash && u.vout === utxo.outputIndex, 58 | ); 59 | // shouldn't happen since utxos should be subset of trezorUtxos (with different shape/fields) 60 | if (!utxoWithPath) 61 | throw Error(`Cannot transform utxo ${utxo.txHash}:${utxo.outputIndex}`); 62 | 63 | return { 64 | path: utxoWithPath.path, 65 | prev_hash: utxo.txHash, 66 | prev_index: utxo.outputIndex, 67 | }; 68 | }); 69 | }; 70 | 71 | export const transformToTrezorOutputs = ( 72 | outputs: FinalOutput[], 73 | changeAddressParameters: CardanoAddressParameters, 74 | ): CardanoOutput[] => { 75 | return outputs.map(output => { 76 | let params: 77 | | { address: string } 78 | | { addressParameters: CardanoAddressParameters }; 79 | 80 | if (output.isChange) { 81 | params = { 82 | addressParameters: changeAddressParameters, 83 | }; 84 | } else { 85 | params = { 86 | address: output.address, 87 | }; 88 | } 89 | 90 | return { 91 | ...params, 92 | amount: output.amount, 93 | tokenBundle: transformToTokenBundle(output.assets), 94 | }; 95 | }); 96 | }; 97 | 98 | export const drepIdToHex = ( 99 | drepId: string, 100 | ): { 101 | type: CardanoDRepType.KEY_HASH | CardanoDRepType.SCRIPT_HASH; 102 | hex: string; 103 | } => { 104 | const drep = CardanoWasm.DRep.from_bech32(drepId); 105 | const kind = drep.kind() as unknown as 106 | | CardanoDRepType.KEY_HASH 107 | | CardanoDRepType.SCRIPT_HASH; 108 | 109 | let drepHex: string | undefined; 110 | switch (kind) { 111 | case CardanoDRepType.KEY_HASH: 112 | drepHex = drep.to_key_hash()?.to_hex(); 113 | break; 114 | case CardanoDRepType.SCRIPT_HASH: 115 | drepHex = drep.to_script_hash()?.to_hex(); 116 | break; 117 | } 118 | 119 | if (!drepHex) { 120 | throw Error('Invalid drepId'); 121 | } 122 | 123 | const drepData = { 124 | type: kind, 125 | hex: drepHex, 126 | }; 127 | drep.free(); 128 | return drepData; 129 | }; 130 | -------------------------------------------------------------------------------- /src/types/types.ts: -------------------------------------------------------------------------------- 1 | import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs'; 2 | import { BigNum } from '@emurgo/cardano-serialization-lib-nodejs'; 3 | import { CertificateType } from '../constants'; 4 | 5 | export interface Asset { 6 | unit: string; 7 | quantity: string; 8 | } 9 | export interface Utxo { 10 | address: string; 11 | txHash: string; 12 | outputIndex: number; 13 | amount: Asset[]; 14 | } 15 | 16 | export interface CardanoCertificatePointer { 17 | blockIndex: number; 18 | txIndex: number; 19 | certificateIndex: number; 20 | } 21 | 22 | export interface BaseOutput { 23 | setMax?: boolean; 24 | isChange?: boolean; 25 | assets: Asset[]; 26 | } 27 | 28 | export interface ExternalOutput extends BaseOutput { 29 | amount: string; 30 | address: string; 31 | setMax?: false; 32 | } 33 | 34 | export interface ExternalOutputIncomplete extends BaseOutput { 35 | amount?: string | undefined; 36 | address?: string; 37 | setMax: boolean; 38 | } 39 | 40 | export interface ChangeOutput extends BaseOutput { 41 | amount: string; 42 | address: string; 43 | isChange: true; 44 | } 45 | 46 | export type FinalOutput = ExternalOutput | ChangeOutput; 47 | export type UserOutput = ExternalOutput | ExternalOutputIncomplete; 48 | export type Output = FinalOutput | ExternalOutputIncomplete; 49 | 50 | export interface OutputCost { 51 | output: CardanoWasm.TransactionOutput; 52 | outputFee: BigNum; 53 | minOutputAmount: BigNum; 54 | } 55 | 56 | export enum CardanoAddressType { 57 | BASE = 0, 58 | BASE_SCRIPT_KEY = 1, 59 | BASE_KEY_SCRIPT = 2, 60 | BASE_SCRIPT_SCRIPT = 3, 61 | POINTER = 4, 62 | POINTER_SCRIPT = 5, 63 | ENTERPRISE = 6, 64 | ENTERPRISE_SCRIPT = 7, 65 | BYRON = 8, 66 | REWARD = 14, 67 | REWARD_SCRIPT = 15, 68 | } 69 | 70 | export enum CardanoDRepType { 71 | KEY_HASH = 0, 72 | SCRIPT_HASH = 1, 73 | ABSTAIN = 2, 74 | NO_CONFIDENCE = 3, 75 | } 76 | 77 | export type DRep = 78 | | { 79 | type: CardanoDRepType.KEY_HASH; 80 | keyHash: string; 81 | } 82 | | { 83 | type: CardanoDRepType.SCRIPT_HASH; 84 | scriptHash: string; 85 | } 86 | | { 87 | type: CardanoDRepType.ABSTAIN | CardanoDRepType.NO_CONFIDENCE; 88 | keyHash?: never; 89 | scriptHash?: never; 90 | }; 91 | 92 | export interface CoinSelectionResult { 93 | tx: { body: string; hash: string; size: number }; 94 | inputs: Utxo[]; 95 | outputs: Output[]; 96 | fee: string; 97 | totalSpent: string; 98 | deposit: string; 99 | withdrawal: string; 100 | ttl?: number; 101 | max?: string; 102 | } 103 | 104 | export type PrecomposedTransaction = 105 | | ({ 106 | type: 'final'; 107 | outputs: FinalOutput[]; 108 | } & Omit) 109 | | ({ 110 | type: 'nonfinal'; 111 | } & Pick< 112 | CoinSelectionResult, 113 | 'fee' | 'totalSpent' | 'deposit' | 'withdrawal' | 'max' 114 | >); 115 | 116 | export interface Withdrawal { 117 | stakeAddress: string; 118 | amount: string; 119 | } 120 | 121 | export type CertificateTypeType = typeof CertificateType; 122 | 123 | export interface CertificateStakeRegistration { 124 | type: 125 | | CertificateTypeType['STAKE_REGISTRATION'] 126 | | CertificateTypeType['STAKE_DEREGISTRATION']; 127 | stakingKeyHash?: string; 128 | } 129 | 130 | export interface CertificateStakeDelegation { 131 | type: CertificateTypeType['STAKE_DELEGATION']; 132 | stakingKeyHash?: string; 133 | pool: string; 134 | } 135 | 136 | export interface CertificateStakePoolRegistration { 137 | type: CertificateTypeType['STAKE_POOL_REGISTRATION']; 138 | pool_parameters: Record; 139 | } 140 | 141 | export interface CertificateVoteDelegation { 142 | type: CertificateTypeType['VOTE_DELEGATION']; 143 | dRep: DRep; 144 | } 145 | 146 | export type Certificate = 147 | | CertificateStakeRegistration 148 | | CertificateStakeDelegation 149 | | CertificateStakePoolRegistration 150 | | CertificateVoteDelegation; 151 | 152 | export interface Options { 153 | feeParams?: { a: string }; 154 | debug?: boolean; 155 | forceLargestFirstSelection?: boolean; 156 | _maxTokensPerOutput?: number; 157 | } 158 | 159 | export interface CoinSelectionParams { 160 | utxos: Utxo[]; 161 | outputs: UserOutput[]; 162 | changeAddress: string; 163 | certificates: Certificate[]; 164 | withdrawals: Withdrawal[]; 165 | accountPubKey: string; 166 | ttl?: number; 167 | } 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coin-selection 2 | 3 | Minimal implementation of Cardano coin selection algorithms (see [CIP-2](https://cips.cardano.org/cips/cip2/)) developed solely for Trezor Suite. 4 | Under the hood it leverages Cardano Serialization Lib via WASM module. 5 | 6 | ## Features 7 | 8 | - Final tx plan: Compose transaction plan for given inputs (shelley utxos only), certificates (support for stake (de)registration and delegation), withdrawals,... 9 | - Draft tx plan: Return basic information about potential transaction (such as a size and fee) even for incomplete inputs (without an recipient address or an amount) 10 | - Set max: Calculate the max amount of an asset that is possible to include in a transaction output 11 | 12 | ## Usage 13 | 14 | ```typescript 15 | const txPlan = coinSelection( 16 | txParams: { 17 | utxos: Utxo[]; 18 | outputs: UserOutput[]; 19 | changeAddress: string; 20 | certificates: Certificate[]; 21 | withdrawals: Withdrawal[]; 22 | accountPubKey: string; 23 | ttl?: number; 24 | }, 25 | options: { 26 | feeParams?: { a: string }; 27 | debug?: boolean; 28 | forceLargestFirstSelection?: boolean; 29 | } 30 | ); 31 | ``` 32 | 33 | ### `coinSelection(txParams, options)` 34 | 35 | #### `txParams` 36 | 37 | - `utxos`: Array of account's utxo 38 | - `outputs`: Requested outputs provided by an user 39 | - `changeAddress`: An address where the change will be sent 40 | - `certificates`: Stake registration and/or delegation certificates (stake pool registration is not supported) 41 | - `withdrawals`: Withdrawal requests 42 | - `accountPubKey`: Account public key 43 | - `ttl`: Time-to-live for the transaction 44 | 45 | #### `Options` 46 | 47 | - `forceLargestFirstSelection`: Always use largest-first algorithm 48 | - `debug`: print debug information about coin-selection (selected utxos, outputs including change output,...) 49 | 50 | ## Example 51 | 52 | ### Final tx plan 53 | 54 | ```typescript 55 | 56 | export const utxo1 = { 57 | address: 58 | 'addr1q860vxljhadqxnrrsr2j6yxnwpdkyquq74lmghx502aj0r28d2kd47hsre5v9urjyu8s0ryk38dxzw0t5jesncw4v90sp0878u', 59 | txHash: '3c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d', 60 | outputIndex: 0, 61 | amount: [ 62 | { 63 | quantity: '5000000', 64 | unit: 'lovelace', 65 | }, 66 | ], 67 | }; 68 | 69 | export const utxo2 = { 70 | address: 71 | 'addr1q860vxljhadqxnrrsr2j6yxnwpdkyquq74lmghx502aj0r28d2kd47hsre5v9urjyu8s0ryk38dxzw0t5jesncw4v90sp0878u', 72 | txHash: '9e63fddf20cb7b5472e2c9a1bb4bbe3112b8f2b22e45bc441206bcddde5c58a0', 73 | outputIndex: 1, 74 | amount: [ 75 | { 76 | quantity: '5000000', 77 | unit: 'lovelace', 78 | }, 79 | { 80 | quantity: '1000', 81 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 82 | }, 83 | ], 84 | }; 85 | 86 | const txPlan = coinSelection( 87 | txParams: { 88 | utxos: Utxo[]; 89 | outputs: [ 90 | { 91 | address: 92 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 93 | amount: '1000000', 94 | assets: [], 95 | setMax: false, 96 | }, 97 | { 98 | address: 99 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 100 | amount: undefined, 101 | assets: [], 102 | setMax: true, 103 | }, 104 | ], 105 | changeAddress: `addr1q8u2f05rprqjhygz22m06mhy4xrnqvqqpyuzhmxqfxnwvxz8d2kd47hsre5v9urjyu8s0ryk38dxzw0t5jesncw4v90s22tk0f`, 106 | certificates: [], 107 | withdrawals: [], 108 | accountPubKey: 109 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 110 | }, 111 | ); 112 | 113 | // txPlan 114 | // { 115 | // max: '7480901', 116 | // totalSpent: '8655202', 117 | // fee: '174301', 118 | // inputs: [utxo1, utxo2], 119 | // outputs: [ 120 | // { 121 | // address: 122 | // 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 123 | // amount: '1000000', 124 | // assets: [], 125 | // setMax: false, 126 | // }, 127 | // { 128 | // address: 129 | // 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 130 | // amount: '7480901', 131 | // assets: [], 132 | // setMax: true, 133 | // }, 134 | // { 135 | // isChange: true, 136 | // address: changeAddress, 137 | // amount: '1344798', 138 | // assets: [ 139 | // { 140 | // quantity: '1000', 141 | // unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 142 | // }, 143 | // ], 144 | // }, 145 | // ], 146 | // }, 147 | ; 148 | ``` 149 | 150 | ### Draft tx plan 151 | 152 | ```typescript 153 | 154 | const txPlan = coinSelection( 155 | { 156 | utxos: [utxo1]; 157 | outputs: [ 158 | { 159 | address: undefined, // address not filled 160 | amount: '2000000', 161 | assets: [], 162 | setMax: false, 163 | }, 164 | ], 165 | changeAddress: `addr1q8u2f05rprqjhygz22m06mhy4xrnqvqqpyuzhmxqfxnwvxz8d2kd47hsre5v9urjyu8s0ryk38dxzw0t5jesncw4v90s22tk0f`, 166 | certificates: [], 167 | withdrawals: [], 168 | accountPubKey: 169 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 170 | }, 171 | ); 172 | 173 | // txPlan: 174 | // { 175 | // totalSpent: '2168053', 176 | // fee: '168053', 177 | // }, 178 | 179 | ``` 180 | 181 | ## Notes 182 | 183 | - If there are certificates present or the transaction includes an output with `setMax: true` then largest-first algorithm will be used instead of random-improve. 184 | -------------------------------------------------------------------------------- /tests/fixtures/constants.ts: -------------------------------------------------------------------------------- 1 | import { Utxo } from '../../src/types/types'; 2 | 3 | export const prepareUtxo = (utxo: Utxo, update: Partial): Utxo => { 4 | return { 5 | ...utxo, 6 | ...update, 7 | }; 8 | }; 9 | 10 | export const changeAddress = 11 | 'addr1q8u2f05rprqjhygz22m06mhy4xrnqvqqpyuzhmxqfxnwvxz8d2kd47hsre5v9urjyu8s0ryk38dxzw0t5jesncw4v90s22tk0f'; 12 | 13 | export const utxo1 = Object.freeze({ 14 | address: 15 | 'addr1q860vxljhadqxnrrsr2j6yxnwpdkyquq74lmghx502aj0r28d2kd47hsre5v9urjyu8s0ryk38dxzw0t5jesncw4v90sp0878u', 16 | txHash: '3c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d', 17 | outputIndex: 0, 18 | amount: [ 19 | { 20 | quantity: '5000000', 21 | unit: 'lovelace', 22 | }, 23 | ], 24 | }); 25 | 26 | export const utxo2 = Object.freeze({ 27 | address: 28 | 'addr1q860vxljhadqxnrrsr2j6yxnwpdkyquq74lmghx502aj0r28d2kd47hsre5v9urjyu8s0ryk38dxzw0t5jesncw4v90sp0878u', 29 | txHash: '9e63fddf20cb7b5472e2c9a1bb4bbe3112b8f2b22e45bc441206bcddde5c58a0', 30 | outputIndex: 1, 31 | amount: [ 32 | { 33 | quantity: '5000000', 34 | unit: 'lovelace', 35 | }, 36 | { 37 | quantity: '1000', 38 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 39 | }, 40 | ], 41 | }); 42 | 43 | export const utxo3 = Object.freeze({ 44 | ...prepareUtxo(utxo1, { 45 | outputIndex: 2, 46 | amount: [ 47 | { 48 | quantity: '10000000', 49 | unit: 'lovelace', 50 | }, 51 | ], 52 | }), 53 | }); 54 | 55 | export const utxo4 = Object.freeze({ 56 | ...prepareUtxo(utxo1, { 57 | outputIndex: 3, 58 | amount: [ 59 | { 60 | quantity: '2000000', 61 | unit: 'lovelace', 62 | }, 63 | ], 64 | }), 65 | }); 66 | export const utxo5 = Object.freeze({ 67 | ...prepareUtxo(utxo1, { 68 | outputIndex: 4, 69 | amount: [ 70 | { 71 | quantity: '1000000', 72 | unit: 'lovelace', 73 | }, 74 | ], 75 | }), 76 | }); 77 | 78 | export const utxo6 = Object.freeze({ 79 | ...prepareUtxo(utxo2, { 80 | outputIndex: 7, 81 | amount: [ 82 | { 83 | quantity: '4000000', 84 | unit: 'lovelace', 85 | }, 86 | { 87 | quantity: '2000', 88 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 89 | }, 90 | { 91 | quantity: '100', 92 | unit: 'c6207cbbc916fa3bbb4b91cc7789c7d7ddfb84264fa76f7ee627a9d8', 93 | }, 94 | ], 95 | }), 96 | }); 97 | 98 | export const utxo7 = Object.freeze({ 99 | ...prepareUtxo(utxo2, { 100 | outputIndex: 8, 101 | amount: [ 102 | { 103 | quantity: '1410000', 104 | unit: 'lovelace', 105 | }, 106 | { 107 | quantity: '1000', 108 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 109 | }, 110 | ], 111 | }), 112 | }); 113 | 114 | export const utxo8 = Object.freeze({ 115 | ...prepareUtxo(utxo2, { 116 | outputIndex: 7, 117 | amount: [ 118 | { 119 | quantity: '2000000', 120 | unit: 'lovelace', 121 | }, 122 | { 123 | quantity: '2000', 124 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 125 | }, 126 | { 127 | quantity: '100', 128 | unit: 'c6207cbbc916fa3bbb4b91cc7789c7d7ddfb84264fa76f7ee627a9d8', 129 | }, 130 | ], 131 | }), 132 | }); 133 | 134 | export const setMaxAdaInputs = [ 135 | { 136 | address: 137 | 'addr_test1qzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsu8d9w5', 138 | txHash: 'd6de3f33c3b421167eb1726c48129990ec16512dd829ad2239751ba49773b30c', 139 | outputIndex: 2, 140 | amount: [ 141 | { 142 | quantity: '2611207363', 143 | unit: 'lovelace', 144 | }, 145 | ], 146 | }, 147 | { 148 | address: 149 | 'addr_test1qzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsu8d9w5', 150 | txHash: 'd6de3f33c3b421167eb1726c48129990ec16512dd829ad2239751ba49773b30c', 151 | outputIndex: 5, 152 | amount: [ 153 | { 154 | quantity: '1305603682', 155 | unit: 'lovelace', 156 | }, 157 | ], 158 | }, 159 | { 160 | address: 161 | 'addr_test1qzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsu8d9w5', 162 | txHash: 'd6de3f33c3b421167eb1726c48129990ec16512dd829ad2239751ba49773b30c', 163 | outputIndex: 8, 164 | amount: [ 165 | { 166 | quantity: '652801841', 167 | unit: 'lovelace', 168 | }, 169 | ], 170 | }, 171 | { 172 | address: 173 | 'addr_test1qzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsu8d9w5', 174 | txHash: 'd6de3f33c3b421167eb1726c48129990ec16512dd829ad2239751ba49773b30c', 175 | outputIndex: 11, 176 | amount: [ 177 | { 178 | quantity: '326400920', 179 | unit: 'lovelace', 180 | }, 181 | ], 182 | }, 183 | { 184 | address: 185 | 'addr_test1qzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsu8d9w5', 186 | txHash: 'd6de3f33c3b421167eb1726c48129990ec16512dd829ad2239751ba49773b30c', 187 | outputIndex: 14, 188 | amount: [ 189 | { 190 | quantity: '163200460', 191 | unit: 'lovelace', 192 | }, 193 | ], 194 | }, 195 | { 196 | address: 197 | 'addr_test1qzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsu8d9w5', 198 | txHash: 'd6de3f33c3b421167eb1726c48129990ec16512dd829ad2239751ba49773b30c', 199 | outputIndex: 17, 200 | amount: [ 201 | { 202 | quantity: '81600230', 203 | unit: 'lovelace', 204 | }, 205 | ], 206 | }, 207 | { 208 | address: 209 | 'addr_test1qzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsu8d9w5', 210 | txHash: 'd6de3f33c3b421167eb1726c48129990ec16512dd829ad2239751ba49773b30c', 211 | outputIndex: 21, 212 | amount: [ 213 | { 214 | quantity: '40800115', 215 | unit: 'lovelace', 216 | }, 217 | ], 218 | }, 219 | { 220 | address: 221 | 'addr_test1qzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsu8d9w5', 222 | txHash: 'd6de3f33c3b421167eb1726c48129990ec16512dd829ad2239751ba49773b30c', 223 | outputIndex: 22, 224 | amount: [ 225 | { 226 | quantity: '40800115', 227 | unit: 'lovelace', 228 | }, 229 | ], 230 | }, 231 | ]; 232 | -------------------------------------------------------------------------------- /tests/utils/fixtures/common.ts: -------------------------------------------------------------------------------- 1 | const utxo1 = { 2 | address: 'addr1', 3 | txHash: 'hash1', 4 | outputIndex: 0, 5 | amount: [ 6 | { 7 | unit: 'lovelace', 8 | quantity: '1000000', 9 | }, 10 | ], 11 | }; 12 | 13 | const utxo2 = { 14 | address: 'addr1', 15 | txHash: 'hash1', 16 | outputIndex: 0, 17 | amount: [ 18 | { 19 | unit: 'lovelace', 20 | quantity: '1000000', 21 | }, 22 | { 23 | unit: 'token1', 24 | quantity: '1000000', 25 | }, 26 | ], 27 | }; 28 | 29 | const utxo3 = { 30 | address: 'addr1', 31 | txHash: 'hash1', 32 | outputIndex: 0, 33 | amount: [ 34 | { 35 | unit: 'lovelace', 36 | quantity: '1000000', 37 | }, 38 | { 39 | unit: 'token2', 40 | quantity: '1000000', 41 | }, 42 | ], 43 | }; 44 | 45 | export const filterUtxos = [ 46 | { 47 | description: 'filter lovelace', 48 | utxos: [utxo1, utxo2, utxo3], 49 | asset: 'lovelace', 50 | result: [utxo1, utxo2, utxo3], 51 | }, 52 | { 53 | description: 'filter token1', 54 | utxos: [utxo1, utxo2, utxo3], 55 | asset: 'token1', 56 | result: [utxo2], 57 | }, 58 | { 59 | description: 'filter token2', 60 | utxos: [utxo1, utxo2, utxo3], 61 | asset: 'token2', 62 | result: [utxo3], 63 | }, 64 | { 65 | description: 'filter non existent token', 66 | utxos: [utxo1, utxo2, utxo3], 67 | asset: 'token3', 68 | result: [], 69 | }, 70 | ]; 71 | 72 | export const buildTxOutput = [ 73 | { 74 | description: 'Byron address output', 75 | output: { 76 | address: 77 | '37btjrVyb4KDXBNC4haBVPCrro8AQPHwvCMp3RFhhSVWwfFmZ6wwzSK6JK1hY6wHNmtrpTf1kdbva8TCneM2YsiXT7mrzT21EacHnPpz5YyUdj64na', 78 | amount: '5000000', 79 | assets: [], 80 | setMax: false, 81 | }, 82 | dummyAddress: 83 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 84 | asset: 'lovelace', 85 | result: { 86 | address: 87 | '37btjrVyb4KDXBNC4haBVPCrro8AQPHwvCMp3RFhhSVWwfFmZ6wwzSK6JK1hY6wHNmtrpTf1kdbva8TCneM2YsiXT7mrzT21EacHnPpz5YyUdj64na', 88 | amount: '5000000', 89 | assets: [], 90 | }, 91 | }, 92 | { 93 | description: 'Shelley address output with same policy assets', 94 | output: { 95 | address: 96 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 97 | amount: '5000000', 98 | assets: [ 99 | { 100 | quantity: '5000', 101 | unit: '57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf391652243484f43', 102 | }, 103 | { 104 | quantity: '10000', 105 | unit: '57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf39165224d494e54', 106 | }, 107 | { 108 | quantity: '1000000', 109 | unit: '57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf3916522534245525259', 110 | }, 111 | ], 112 | setMax: false, 113 | }, 114 | dummyAddress: 115 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 116 | asset: 'lovelace', 117 | result: { 118 | address: 119 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 120 | amount: '5000000', 121 | assets: [ 122 | { 123 | quantity: '5000', 124 | unit: '57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf391652243484f43', 125 | }, 126 | { 127 | quantity: '10000', 128 | unit: '57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf39165224d494e54', 129 | }, 130 | { 131 | quantity: '1000000', 132 | unit: '57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf3916522534245525259', 133 | }, 134 | ], 135 | }, 136 | }, 137 | ]; 138 | 139 | export const orderInputs = [ 140 | { 141 | description: 'reorder inputs to match order in txbody', 142 | inputsToOrder: [ 143 | { 144 | address: 145 | 'addr1qy4xpnf4lk560dgrds5zsunh6xdssg94c5sc8dqdclcn2fdl85agr52j3ffkwzq2yasu59ccwvfj39kel85ng3u7lhlq4e4m4l', 146 | txHash: 147 | '9ed3ef581f545f2143eca490d7f20a511100add747bb3d651cc2aa5815f77b1d', 148 | outputIndex: 1, 149 | amount: [ 150 | { 151 | quantity: '1344974', 152 | unit: 'lovelace', 153 | }, 154 | { 155 | quantity: '5675656536', 156 | unit: '9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d7753554e444145', 157 | }, 158 | ], 159 | }, 160 | { 161 | address: 162 | 'addr1q860vxljhadqxnrrsr2j6yxnwpdkyquq74lmghx502aj0r28d2kd47hsre5v9urjyu8s0ryk38dxzw0t5jesncw4v90sp0878u', 163 | txHash: 164 | '06227a5ee5640d26224470ad195c82941bfa49386a85149c09c465c4edb0edc0', 165 | outputIndex: 0, 166 | amount: [ 167 | { 168 | quantity: '10000000', 169 | unit: 'lovelace', 170 | }, 171 | ], 172 | }, 173 | ], 174 | txBodyHex: 175 | 'a4008282582006227a5ee5640d26224470ad195c82941bfa49386a85149c09c465c4edb0edc0008258209ed3ef581f545f2143eca490d7f20a511100add747bb3d651cc2aa5815f77b1d010182825839013af9d8434bea8de03cd698d5fa1c6b82b991146a755f509e95d6b53b15ab05b40d24d39c9d14dfec04d87ed071f2c66484b3ab83ab3d603d821a0012050ca1581c9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77a14653554e4441451b00000001524ba55882583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f1a009865cd021a0002b175031a03f7e7bf', 176 | result: [ 177 | { 178 | address: 179 | 'addr1q860vxljhadqxnrrsr2j6yxnwpdkyquq74lmghx502aj0r28d2kd47hsre5v9urjyu8s0ryk38dxzw0t5jesncw4v90sp0878u', 180 | txHash: 181 | '06227a5ee5640d26224470ad195c82941bfa49386a85149c09c465c4edb0edc0', 182 | outputIndex: 0, 183 | amount: [ 184 | { 185 | quantity: '10000000', 186 | unit: 'lovelace', 187 | }, 188 | ], 189 | }, 190 | { 191 | address: 192 | 'addr1qy4xpnf4lk560dgrds5zsunh6xdssg94c5sc8dqdclcn2fdl85agr52j3ffkwzq2yasu59ccwvfj39kel85ng3u7lhlq4e4m4l', 193 | txHash: 194 | '9ed3ef581f545f2143eca490d7f20a511100add747bb3d651cc2aa5815f77b1d', 195 | outputIndex: 1, 196 | amount: [ 197 | { 198 | quantity: '1344974', 199 | unit: 'lovelace', 200 | }, 201 | { 202 | quantity: '5675656536', 203 | unit: '9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d7753554e444145', 204 | }, 205 | ], 206 | }, 207 | ], 208 | }, 209 | ]; 210 | -------------------------------------------------------------------------------- /src/methods/randomImprove.ts: -------------------------------------------------------------------------------- 1 | import { ERROR } from '../constants'; 2 | import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs'; 3 | import { 4 | CoinSelectionParams, 5 | CoinSelectionResult, 6 | Options, 7 | Output, 8 | OutputCost, 9 | UserOutput, 10 | Utxo, 11 | } from '../types/types'; 12 | import { 13 | bigNumFromStr, 14 | prepareChangeOutput, 15 | setMinUtxoValueForOutputs, 16 | getTxBuilder, 17 | getUserOutputQuantityWithDeposit, 18 | multiAssetToArray, 19 | buildTxInput, 20 | buildTxOutput, 21 | getUnsatisfiedAssets, 22 | splitChangeOutput, 23 | filterUtxos, 24 | getUtxoQuantity, 25 | getOutputQuantity, 26 | getRandomUtxo, 27 | orderInputs, 28 | } from '../utils/common'; 29 | import { CoinSelectionError } from '../utils/errors'; 30 | import { getLogger } from '../utils/logger'; 31 | // Heavily inspired by https://github.com/input-output-hk/cardano-js-sdk 32 | 33 | const improvesSelection = ( 34 | utxoAlreadySelected: Utxo[], 35 | input: Utxo, 36 | minimumTarget: CardanoWasm.BigNum, 37 | asset: string, 38 | ): boolean => { 39 | const oldQuantity = getUtxoQuantity(utxoAlreadySelected, asset); 40 | // We still haven't reached the minimum target of 41 | // 100%. Therefore, we consider any potential input 42 | // to be an improvement: 43 | if (oldQuantity.compare(minimumTarget) < 0) return true; 44 | const newQuantity = oldQuantity.checked_add(getUtxoQuantity([input], asset)); 45 | const idealTarget = minimumTarget.checked_mul(bigNumFromStr('2')); 46 | const newDistance = 47 | idealTarget.compare(newQuantity) > 0 48 | ? idealTarget.clamped_sub(newQuantity) 49 | : newQuantity.clamped_sub(idealTarget); 50 | const oldDistance = 51 | idealTarget.compare(oldQuantity) > 0 52 | ? idealTarget.clamped_sub(oldQuantity) 53 | : oldQuantity.clamped_sub(idealTarget); 54 | // Using this input will move us closer to the 55 | // ideal target of 200%, so we treat this as an improvement: 56 | if (newDistance.compare(oldDistance) < 0) return true; 57 | // Adding the selected input would move us further 58 | // away from the target of 200%. Reaching this case 59 | // means we have already covered the minimum target 60 | // of 100%, and therefore it is safe to not consider 61 | // this token any further: 62 | return false; 63 | }; 64 | 65 | const selection = ( 66 | utxos: Utxo[], 67 | outputs: UserOutput[], 68 | txBuilder: CardanoWasm.TransactionBuilder, 69 | dummyAddress: string, 70 | ) => { 71 | const utxoSelected: Utxo[] = []; 72 | const utxoRemaining = JSON.parse(JSON.stringify(utxos)) as Utxo[]; 73 | const preparedOutputs = setMinUtxoValueForOutputs( 74 | txBuilder, 75 | outputs, 76 | dummyAddress, 77 | ); 78 | preparedOutputs.forEach(output => { 79 | const txOutput = buildTxOutput(output, dummyAddress); 80 | txBuilder.add_output(txOutput); 81 | }); 82 | // Check for UTXO_BALANCE_INSUFFICIENT comparing provided inputs with requested outputs 83 | const assetsRemaining = getUnsatisfiedAssets(utxoSelected, preparedOutputs); 84 | assetsRemaining.forEach(asset => { 85 | const outputQuantity = getOutputQuantity(preparedOutputs, asset); 86 | const utxosQuantity = getUtxoQuantity(utxos, asset); 87 | if (outputQuantity.compare(utxosQuantity) > 0) { 88 | throw new CoinSelectionError(ERROR.UTXO_BALANCE_INSUFFICIENT); 89 | } 90 | }); 91 | 92 | while (assetsRemaining.length > 0) { 93 | assetsRemaining.forEach((asset, assetIndex) => { 94 | const assetUtxos = filterUtxos(utxoRemaining, asset); 95 | if (assetUtxos.length > 0) { 96 | const inputIdx = Math.floor(Math.random() * assetUtxos.length); 97 | const utxo = assetUtxos[inputIdx]; 98 | 99 | if ( 100 | improvesSelection( 101 | utxoSelected, 102 | utxo, 103 | getOutputQuantity(preparedOutputs, asset), 104 | asset, 105 | ) 106 | ) { 107 | utxoSelected.push(utxo); 108 | const { input, address, amount } = buildTxInput(utxo); 109 | txBuilder.add_regular_input(address, input, amount); 110 | utxoRemaining.splice(utxoRemaining.indexOf(utxo), 1); 111 | } else { 112 | // The selection was not improved by including 113 | // this input. If we've reached this point, it 114 | // means that we've already covered the minimum 115 | // target of 100%, and therefore it is safe to 116 | // not consider this token any further. 117 | assetsRemaining.splice(assetIndex, 1); 118 | } 119 | } else { 120 | // The attempt to select an input failed (there were 121 | // no inputs remaining that contained the token). 122 | // This means that we've already covered the minimum 123 | // quantity required (due to the pre-condition), and 124 | // therefore it is safe to not consider this token 125 | // any further: 126 | assetsRemaining.splice(assetIndex, 1); 127 | } 128 | }); 129 | } 130 | return { utxoSelected, utxoRemaining, preparedOutputs }; 131 | }; 132 | 133 | const calculateChange = ( 134 | utxoSelected: Utxo[], 135 | utxoRemaining: Utxo[], 136 | preparedOutputs: UserOutput[], 137 | changeAddress: string, 138 | maxTokensPerOutput: number | undefined, 139 | txBuilder: CardanoWasm.TransactionBuilder, 140 | ): { changeOutputs: OutputCost[] } => { 141 | const totalFeesAmount = txBuilder.min_fee(); 142 | const totalUserOutputsAmount = getUserOutputQuantityWithDeposit( 143 | preparedOutputs, 144 | 0, 145 | ); 146 | 147 | const singleChangeOutput = prepareChangeOutput( 148 | txBuilder, 149 | utxoSelected, 150 | preparedOutputs, 151 | changeAddress, 152 | getUtxoQuantity(utxoSelected, 'lovelace'), 153 | getUserOutputQuantityWithDeposit(preparedOutputs, 0), 154 | totalFeesAmount, 155 | () => getRandomUtxo(txBuilder, utxoRemaining, utxoSelected), 156 | ); 157 | 158 | const changeOutputs = singleChangeOutput 159 | ? splitChangeOutput( 160 | txBuilder, 161 | singleChangeOutput, 162 | changeAddress, 163 | maxTokensPerOutput, 164 | ) 165 | : []; 166 | 167 | let requiredAmount = totalFeesAmount.checked_add(totalUserOutputsAmount); 168 | changeOutputs.forEach(changeOutput => { 169 | // we need to cover amounts and fees for change outputs 170 | requiredAmount = requiredAmount 171 | .checked_add(changeOutput.output.amount().coin()) 172 | .checked_add(changeOutput.outputFee); 173 | }); 174 | 175 | if (requiredAmount.compare(getUtxoQuantity(utxoSelected, 'lovelace')) > 0) { 176 | const randomUtxo = getRandomUtxo(txBuilder, utxoRemaining, utxoSelected); 177 | if (randomUtxo?.utxo) { 178 | randomUtxo.addUtxo(); 179 | const { changeOutputs } = calculateChange( 180 | utxoSelected, 181 | utxoRemaining, 182 | preparedOutputs, 183 | changeAddress, 184 | maxTokensPerOutput, 185 | txBuilder, 186 | ); 187 | return { changeOutputs }; 188 | } else { 189 | throw new CoinSelectionError(ERROR.UTXO_BALANCE_INSUFFICIENT); 190 | } 191 | } else { 192 | return { changeOutputs }; 193 | } 194 | }; 195 | 196 | export const randomImprove = ( 197 | params: Pick< 198 | CoinSelectionParams, 199 | 'utxos' | 'outputs' | 'changeAddress' | 'ttl' 200 | >, 201 | options?: Options, 202 | ): CoinSelectionResult => { 203 | const { utxos, outputs, changeAddress, ttl } = params; 204 | const logger = getLogger(!!options?.debug); 205 | if (outputs.length > utxos.length) { 206 | logger.debug( 207 | 'There are more outputs than utxos. Random-improve alg needs to have number of utxos same or larger than number of outputs', 208 | ); 209 | throw new CoinSelectionError(ERROR.UTXO_NOT_FRAGMENTED_ENOUGH); 210 | } 211 | const txBuilder = getTxBuilder(options?.feeParams?.a); 212 | if (ttl) { 213 | txBuilder.set_ttl(ttl); 214 | } 215 | 216 | const { utxoSelected, utxoRemaining, preparedOutputs } = selection( 217 | utxos, 218 | outputs, 219 | txBuilder, 220 | changeAddress, 221 | ); 222 | 223 | // compute change and adjust for fee 224 | const { changeOutputs } = calculateChange( 225 | utxoSelected, 226 | utxoRemaining, 227 | preparedOutputs, 228 | changeAddress, 229 | options?._maxTokensPerOutput, 230 | txBuilder, 231 | ); 232 | 233 | const finalOutputs: Output[] = JSON.parse(JSON.stringify(preparedOutputs)); 234 | changeOutputs.forEach(change => { 235 | const ch = { 236 | isChange: true, 237 | amount: change.output.amount().coin().to_str(), 238 | address: changeAddress, 239 | assets: multiAssetToArray(change.output.amount().multiasset()), 240 | }; 241 | finalOutputs.push(ch); 242 | txBuilder.add_output(buildTxOutput(ch, changeAddress)); 243 | }); 244 | 245 | const totalUserOutputsAmount = getUserOutputQuantityWithDeposit( 246 | preparedOutputs, 247 | 0, 248 | ); 249 | 250 | const totalInput = getUtxoQuantity(utxoSelected, 'lovelace'); 251 | const totalOutput = getOutputQuantity(finalOutputs, 'lovelace'); 252 | const fee = totalInput.checked_sub(totalOutput); 253 | const totalSpent = totalUserOutputsAmount.checked_add(fee); 254 | 255 | txBuilder.set_fee(fee); 256 | const txBody = txBuilder.build(); 257 | const txHash = CardanoWasm.FixedTransaction.new_from_body_bytes( 258 | txBody.to_bytes(), 259 | ) 260 | .transaction_hash() 261 | .to_hex(); 262 | const txBodyHex = Buffer.from(txBody.to_bytes()).toString('hex'); 263 | 264 | // reorder inputs to match order within tx 265 | const orderedInputs = orderInputs(utxoSelected, txBody); 266 | return { 267 | tx: { body: txBodyHex, hash: txHash, size: txBuilder.full_size() }, 268 | inputs: orderedInputs, 269 | outputs: finalOutputs, 270 | fee: fee.to_str(), 271 | totalSpent: totalSpent.to_str(), 272 | deposit: '0', 273 | withdrawal: '0', 274 | ttl, 275 | }; 276 | }; 277 | -------------------------------------------------------------------------------- /src/methods/largestFirst.ts: -------------------------------------------------------------------------------- 1 | import { ERROR } from '../constants'; 2 | import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs'; 3 | import { 4 | ChangeOutput, 5 | CoinSelectionParams, 6 | CoinSelectionResult, 7 | Options, 8 | Output, 9 | OutputCost, 10 | Utxo, 11 | } from '../types/types'; 12 | import { 13 | bigNumFromStr, 14 | calculateRequiredDeposit, 15 | getAssetAmount, 16 | prepareCertificates, 17 | prepareChangeOutput, 18 | prepareWithdrawals, 19 | setMinUtxoValueForOutputs, 20 | sortUtxos, 21 | getTxBuilder, 22 | getInitialUtxoSet, 23 | setMaxOutput, 24 | getUserOutputQuantityWithDeposit, 25 | multiAssetToArray, 26 | buildTxInput, 27 | buildTxOutput, 28 | getUnsatisfiedAssets, 29 | splitChangeOutput, 30 | calculateUserOutputsFee, 31 | orderInputs, 32 | } from '../utils/common'; 33 | import { CoinSelectionError } from '../utils/errors'; 34 | 35 | export const largestFirst = ( 36 | params: CoinSelectionParams, 37 | options?: Options, 38 | ): CoinSelectionResult => { 39 | const { 40 | utxos, 41 | outputs, 42 | changeAddress, 43 | certificates, 44 | withdrawals, 45 | accountPubKey, 46 | ttl, 47 | } = params; 48 | const txBuilder = getTxBuilder(options?.feeParams?.a); 49 | if (ttl) { 50 | txBuilder.set_ttl(ttl); 51 | } 52 | 53 | const usedUtxos: Utxo[] = []; 54 | let sortedUtxos = sortUtxos(utxos); 55 | const accountKey = CardanoWasm.Bip32PublicKey.from_bytes( 56 | Buffer.from(accountPubKey, 'hex'), 57 | ); 58 | 59 | // add withdrawals and certs to correctly set a fee 60 | const preparedCertificates = prepareCertificates(certificates, accountKey); 61 | const preparedWithdrawals = prepareWithdrawals(withdrawals); 62 | 63 | if (preparedCertificates.len() > 0) { 64 | txBuilder.set_certs(preparedCertificates); 65 | } 66 | if (preparedWithdrawals.len() > 0) { 67 | txBuilder.set_withdrawals(preparedWithdrawals); 68 | } 69 | 70 | // TODO: negative value in case of deregistration (-2000000), but we still need enough utxos to cover fee which can't be (is that right?) paid from returned deposit 71 | const deposit = calculateRequiredDeposit(certificates); 72 | const totalWithdrawal = withdrawals.reduce( 73 | (acc, withdrawal) => acc.checked_add(bigNumFromStr(withdrawal.amount)), 74 | bigNumFromStr('0'), 75 | ); 76 | 77 | // calc initial fee 78 | let totalFeesAmount = txBuilder.min_fee(); 79 | let utxosTotalAmount = totalWithdrawal; 80 | if (deposit < 0) { 81 | // stake deregistration, 2 ADA returned 82 | utxosTotalAmount = utxosTotalAmount.checked_add( 83 | bigNumFromStr(Math.abs(deposit).toString()), 84 | ); 85 | } 86 | 87 | const preparedOutputs = setMinUtxoValueForOutputs( 88 | txBuilder, 89 | outputs, 90 | changeAddress, 91 | ); 92 | 93 | const addUtxoToSelection = (utxo: Utxo) => { 94 | const { input, address, amount } = buildTxInput(utxo); 95 | const fee = txBuilder.fee_for_input(address, input, amount); 96 | txBuilder.add_regular_input(address, input, amount); 97 | usedUtxos.push(utxo); 98 | totalFeesAmount = totalFeesAmount.checked_add(fee); 99 | utxosTotalAmount = utxosTotalAmount.checked_add( 100 | bigNumFromStr(getAssetAmount(utxo)), 101 | ); 102 | }; 103 | 104 | // set initial utxos set for setMax functionality 105 | const maxOutputIndex = outputs.findIndex(o => !!o.setMax); 106 | const maxOutput = preparedOutputs[maxOutputIndex]; 107 | const { used, remaining } = getInitialUtxoSet(sortedUtxos, maxOutput); 108 | sortedUtxos = remaining; 109 | used.forEach(utxo => addUtxoToSelection(utxo)); 110 | 111 | // add cost of external outputs to total fee amount 112 | totalFeesAmount = totalFeesAmount.checked_add( 113 | calculateUserOutputsFee(txBuilder, preparedOutputs, changeAddress), 114 | ); 115 | 116 | let totalUserOutputsAmount = getUserOutputQuantityWithDeposit( 117 | preparedOutputs, 118 | deposit, 119 | ); 120 | 121 | let changeOutput: ChangeOutput[] | null = null; 122 | let sufficientUtxos = false; 123 | let forceAnotherRound = false; 124 | while (!sufficientUtxos) { 125 | if (maxOutput) { 126 | // Reset previously computed maxOutput in order to correctly calculate a potential change output 127 | // when new utxo is added to the set 128 | preparedOutputs[maxOutputIndex] = setMinUtxoValueForOutputs( 129 | txBuilder, 130 | [maxOutput], 131 | changeAddress, 132 | )[0]; 133 | } 134 | 135 | // Calculate change output 136 | let singleChangeOutput: OutputCost | null = prepareChangeOutput( 137 | txBuilder, 138 | usedUtxos, 139 | preparedOutputs, 140 | changeAddress, 141 | utxosTotalAmount, 142 | getUserOutputQuantityWithDeposit(preparedOutputs, deposit), 143 | totalFeesAmount, 144 | ); 145 | 146 | if (maxOutput) { 147 | // set amount for a max output from a changeOutput calculated above 148 | const { maxOutput: newMaxOutput } = setMaxOutput( 149 | txBuilder, 150 | maxOutput, 151 | singleChangeOutput, 152 | ); 153 | 154 | // change output may be completely removed if all ADA are consumed by max output 155 | preparedOutputs[maxOutputIndex] = newMaxOutput; 156 | // recalculate total user outputs amount 157 | totalUserOutputsAmount = getUserOutputQuantityWithDeposit( 158 | preparedOutputs, 159 | deposit, 160 | ); 161 | 162 | // recalculate fees for outputs as cost for max output may be larger than before 163 | totalFeesAmount = txBuilder 164 | .min_fee() 165 | .checked_add( 166 | calculateUserOutputsFee(txBuilder, preparedOutputs, changeAddress), 167 | ); 168 | 169 | // recalculate change after setting amount to max output 170 | singleChangeOutput = prepareChangeOutput( 171 | txBuilder, 172 | usedUtxos, 173 | preparedOutputs, 174 | changeAddress, 175 | utxosTotalAmount, 176 | getUserOutputQuantityWithDeposit(preparedOutputs, deposit), 177 | totalFeesAmount, 178 | ); 179 | } 180 | 181 | const changeOutputs = singleChangeOutput 182 | ? splitChangeOutput( 183 | txBuilder, 184 | singleChangeOutput, 185 | changeAddress, 186 | options?._maxTokensPerOutput, 187 | ) 188 | : []; 189 | 190 | let requiredAmount = totalFeesAmount.checked_add(totalUserOutputsAmount); 191 | changeOutputs.forEach(changeOutput => { 192 | // we need to cover amounts and fees for change outputs 193 | requiredAmount = requiredAmount 194 | .checked_add(changeOutput.output.amount().coin()) 195 | .checked_add(changeOutput.outputFee); 196 | }); 197 | 198 | // List of tokens for which we don't have enough utxos 199 | const unsatisfiedAssets = getUnsatisfiedAssets(usedUtxos, preparedOutputs); 200 | 201 | if ( 202 | utxosTotalAmount.compare(requiredAmount) >= 0 && 203 | unsatisfiedAssets.length === 0 && 204 | usedUtxos.length > 0 && // TODO: force at least 1 utxo, otherwise withdrawal tx is composed without utxo if rewards > tx cost 205 | !forceAnotherRound 206 | ) { 207 | // we are done. we have enough utxos to cover fees + minUtxoValue for each output. now we can add the cost of the change output to total fees 208 | if (changeOutputs.length > 0) { 209 | changeOutputs.forEach(changeOutput => { 210 | totalFeesAmount = totalFeesAmount.checked_add(changeOutput.outputFee); 211 | }); 212 | 213 | // set change output 214 | changeOutput = changeOutputs.map(change => ({ 215 | isChange: true, 216 | amount: change.output.amount().coin().to_str(), 217 | address: changeAddress, 218 | assets: multiAssetToArray(change.output.amount().multiasset()), 219 | })); 220 | } else { 221 | if (sortedUtxos.length > 0) { 222 | // In current iteration we don't have enough utxo to meet min utxo value for an output, 223 | // but some utxos are still available, force adding another one in order to create a change output 224 | forceAnotherRound = true; 225 | continue; 226 | } 227 | 228 | // Change output would be inefficient., we can burn its value + fee we would pay for it 229 | const unspendableChangeAmount = utxosTotalAmount.clamped_sub( 230 | totalFeesAmount.checked_add(totalUserOutputsAmount), 231 | ); 232 | totalFeesAmount = totalFeesAmount.checked_add(unspendableChangeAmount); 233 | } 234 | sufficientUtxos = true; 235 | } else { 236 | if (unsatisfiedAssets.length > 0) { 237 | // TODO: https://github.com/Emurgo/cardano-serialization-lib/pull/264 238 | sortedUtxos = sortUtxos(sortedUtxos, unsatisfiedAssets[0]); 239 | } else { 240 | sortedUtxos = sortUtxos(sortedUtxos); 241 | } 242 | 243 | const utxo = sortedUtxos.shift(); 244 | if (!utxo) break; 245 | addUtxoToSelection(utxo); 246 | forceAnotherRound = false; 247 | } 248 | // END LOOP 249 | } 250 | 251 | if (!sufficientUtxos) { 252 | throw new CoinSelectionError(ERROR.UTXO_BALANCE_INSUFFICIENT); 253 | } 254 | 255 | preparedOutputs.forEach(output => { 256 | const txOutput = buildTxOutput(output, changeAddress); 257 | txBuilder.add_output(txOutput); 258 | }); 259 | 260 | const finalOutputs: Output[] = JSON.parse(JSON.stringify(preparedOutputs)); 261 | if (changeOutput) { 262 | changeOutput.forEach(change => { 263 | finalOutputs.push(change); 264 | txBuilder.add_output(buildTxOutput(change, changeAddress)); 265 | }); 266 | } 267 | 268 | txBuilder.set_fee(totalFeesAmount); 269 | const txBody = txBuilder.build(); 270 | 271 | const txHash = CardanoWasm.FixedTransaction.new_from_body_bytes( 272 | txBody.to_bytes(), 273 | ) 274 | .transaction_hash() 275 | .to_hex(); 276 | const txBodyHex = Buffer.from(txBody.to_bytes()).toString('hex'); 277 | 278 | const totalSpent = totalUserOutputsAmount.checked_add(totalFeesAmount); 279 | 280 | // Set max property with the value of an output which has setMax=true 281 | let max: string | undefined; 282 | if (maxOutput) { 283 | max = 284 | maxOutput.assets.length > 0 285 | ? maxOutput.assets[0].quantity 286 | : maxOutput.amount; 287 | } 288 | 289 | // reorder inputs to match order within tx 290 | const orderedInputs = orderInputs(usedUtxos, txBody); 291 | 292 | return { 293 | tx: { body: txBodyHex, hash: txHash, size: txBuilder.full_size() }, 294 | inputs: orderedInputs, 295 | outputs: finalOutputs, 296 | fee: totalFeesAmount.to_str(), 297 | totalSpent: totalSpent.to_str(), 298 | deposit: deposit.toString(), 299 | withdrawal: totalWithdrawal.to_str(), 300 | ttl, 301 | max, 302 | }; 303 | }; 304 | -------------------------------------------------------------------------------- /tests/methods/fixtures/randomImprove.ts: -------------------------------------------------------------------------------- 1 | import { 2 | changeAddress, 3 | utxo1, 4 | utxo2, 5 | utxo3, 6 | utxo4, 7 | utxo5, 8 | utxo6, 9 | utxo7, 10 | } from '../../fixtures/constants'; 11 | 12 | const UTXO_REAL_SAME_POLICY = [ 13 | { 14 | address: 15 | 'addr_test1qr9jx7pnujcap2chn8zg8gag6nwu00xu6hdd7vv9jc03py0m2tfs2k368ger3n3pngluz0lympuh65rzarw5vux862dse9kvf2', 16 | txHash: '20fd6b27a14ae743a868a1d46f6b484dc8bd886b4644fc96fdcdcf500a52ae92', 17 | outputIndex: 0, 18 | amount: [ 19 | { 20 | quantity: '1344798', 21 | unit: 'lovelace', 22 | }, 23 | { 24 | quantity: '100', 25 | unit: '21c3e7f6f954e606fe90017628b048a0067b561a4f6e2aa0e1aa613156616375756d73', 26 | }, 27 | ], 28 | }, 29 | { 30 | address: 31 | 'addr_test1qr7x37496r3nfc0zmrhc87mu5rtgtmm666ead6r2p840nchm2tfs2k368ger3n3pngluz0lympuh65rzarw5vux862ds9hrc73', 32 | txHash: 'aa2083985bdcc7dc3847703c400146a322881087ad4b1369992f4a74c0bdc098', 33 | outputIndex: 0, 34 | amount: [ 35 | { 36 | quantity: '1758582', 37 | unit: 'lovelace', 38 | }, 39 | { 40 | quantity: '5000', 41 | unit: '57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf391652243484f43', 42 | }, 43 | { 44 | quantity: '10000', 45 | unit: '57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf39165224d494e54', 46 | }, 47 | { 48 | quantity: '1000000', 49 | unit: '57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf3916522534245525259', 50 | }, 51 | { 52 | quantity: '20000', 53 | unit: '57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf391652256414e494c', 54 | }, 55 | { 56 | quantity: '100000', 57 | unit: '769c4c6e9bc3ba5406b9b89fb7beb6819e638ff2e2de63f008d5bcff744e45574d', 58 | }, 59 | ], 60 | }, 61 | { 62 | address: 63 | 'addr_test1qq43pzxxgfdvffrw6jnrej9840nuylaykv7uzcy56t02xv8m2tfs2k368ger3n3pngluz0lympuh65rzarw5vux862dszv2e9w', 64 | txHash: 'e46395fe22896fae4627b21a0343a3d56c2a8a5ca470897ba66ffe675b7212d2', 65 | outputIndex: 1, 66 | amount: [ 67 | { 68 | quantity: '699567638', 69 | unit: 'lovelace', 70 | }, 71 | { 72 | quantity: '1', 73 | unit: '6b8d07d69639e9413dd637a1a815a7323c69c86abbafb66dbfdb1aa7', 74 | }, 75 | ], 76 | }, 77 | ]; 78 | 79 | const restrictedUtxoSet = [ 80 | { 81 | address: 82 | 'addr_test1qq7ajtmfdg0um0ha4e6wgqu33fddfxejxuvj2yfnyz2w3kgle6svh9nacvm632nmcy6fnw9sq85tqkvhagfrhkj9tf6s6jql6l', 83 | txHash: 'cb0cef9f464523a599f8355d2f241147f70dc2f5d559e670910a122053f1538a', 84 | outputIndex: 2, 85 | amount: [ 86 | { 87 | quantity: '1500000', 88 | unit: 'lovelace', 89 | }, 90 | ], 91 | }, 92 | { 93 | address: 94 | 'addr_test1qq7ajtmfdg0um0ha4e6wgqu33fddfxejxuvj2yfnyz2w3kgle6svh9nacvm632nmcy6fnw9sq85tqkvhagfrhkj9tf6s6jql6l', 95 | txHash: 'cb0cef9f464523a599f8355d2f241147f70dc2f5d559e670910a122053f1538a', 96 | outputIndex: 3, 97 | amount: [ 98 | { 99 | quantity: '1300000', 100 | unit: 'lovelace', 101 | }, 102 | ], 103 | }, 104 | ]; 105 | 106 | const utxo8 = { 107 | address: 108 | 'addr_test1qq7ajtmfdg0um0ha4e6wgqu33fddfxejxuvj2yfnyz2w3kgle6svh9nacvm632nmcy6fnw9sq85tqkvhagfrhkj9tf6s6jql6l', 109 | txHash: 'cb0cef9f464523a599f8355d2f241147f70dc2f5d559e670910a122053f1538a', 110 | outputIndex: 8, 111 | amount: [ 112 | { 113 | quantity: '1000000', 114 | unit: 'lovelace', 115 | }, 116 | ], 117 | }; 118 | 119 | export const nonFinalCompose = [ 120 | { 121 | description: 'Non-final compose: amount not filled', 122 | utxos: [utxo1], 123 | outputs: [ 124 | { 125 | address: 126 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 127 | amount: undefined, 128 | assets: [], 129 | setMax: false, 130 | }, 131 | ], 132 | changeAddress: changeAddress, 133 | certificates: [], 134 | withdrawals: [], 135 | accountPubKey: 136 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 137 | options: {}, 138 | result: { 139 | totalSpent: '168317', 140 | fee: '168317', 141 | }, 142 | }, 143 | { 144 | description: 'Non-final compose: address not filled', 145 | utxos: [utxo1], 146 | outputs: [ 147 | { 148 | address: undefined, 149 | amount: '2000000', 150 | assets: [], 151 | setMax: false, 152 | }, 153 | ], 154 | changeAddress: changeAddress, 155 | certificates: [], 156 | withdrawals: [], 157 | accountPubKey: 158 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 159 | options: {}, 160 | result: { 161 | totalSpent: '2168317', 162 | fee: '168317', 163 | }, 164 | }, 165 | { 166 | description: 'Non-final compose, 2 outputs, 1 amount not filled', 167 | utxos: [utxo1, utxo2], 168 | outputs: [ 169 | { 170 | address: 171 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 172 | amount: '6000000', 173 | assets: [], 174 | setMax: false, 175 | }, 176 | { 177 | address: 178 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 179 | amount: undefined, 180 | assets: [], 181 | setMax: false, 182 | }, 183 | ], 184 | changeAddress: changeAddress, 185 | certificates: [], 186 | withdrawals: [], 187 | accountPubKey: 188 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 189 | options: {}, 190 | result: { 191 | totalSpent: '6174565', 192 | fee: '174565', 193 | }, 194 | }, 195 | ]; 196 | 197 | export const coinSelection = [ 198 | { 199 | description: 200 | '2 ADA utxos (2 ADA, 1 ADA), needs both in order to return change and not to burn it as unnecessarily high fee', 201 | utxos: [utxo1, utxo2, utxo3, utxo4, utxo5, utxo6, utxo7], 202 | outputs: [ 203 | { 204 | address: 205 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 206 | amount: '7000000', 207 | assets: [], 208 | setMax: false, 209 | }, 210 | ], 211 | changeAddress: changeAddress, 212 | certificates: [], 213 | withdrawals: [], 214 | accountPubKey: 215 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 216 | ttl: 123456789, 217 | options: {}, 218 | result: { 219 | ttl: 123456789, 220 | // non-deterministic, we rely on sanity check 221 | }, 222 | }, 223 | { 224 | description: 225 | 'Use all utxos to cover outputs, 80000 will be burned as fee because utxos would not cover minUtxoValue for a change output', 226 | utxos: restrictedUtxoSet, 227 | outputs: [ 228 | { 229 | address: 230 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 231 | amount: '2000000', 232 | assets: [], 233 | setMax: false, 234 | }, 235 | ], 236 | changeAddress: changeAddress, 237 | certificates: [], 238 | withdrawals: [], 239 | accountPubKey: 240 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 241 | ttl: undefined, 242 | options: {}, 243 | result: { 244 | totalSpent: '2800000', 245 | fee: '800000', 246 | // inputs: restrictedUtxoSet, // order changes 247 | ttl: undefined, 248 | outputs: [ 249 | { 250 | address: 251 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 252 | amount: '2000000', 253 | assets: [], 254 | setMax: false, 255 | }, 256 | ], 257 | }, 258 | }, 259 | { 260 | description: 261 | 'utxo with several assets, some of them with same policy, change consists of assets from both inputs', 262 | utxos: UTXO_REAL_SAME_POLICY, 263 | outputs: [ 264 | { 265 | address: 266 | 'addr_test1qrv7wa7pj64zthr93ysq6vux5d0spwq0lhfm3wkql77nedxutyh2y8w6cyp6xkwy3fc4zn2p4ceffkc27wz562jx9vyqga7nl9', 267 | amount: '1344798', 268 | assets: [ 269 | { 270 | unit: '21c3e7f6f954e606fe90017628b048a0067b561a4f6e2aa0e1aa613156616375756d73', 271 | quantity: '2', 272 | }, 273 | ], 274 | setMax: false, 275 | }, 276 | { 277 | address: 278 | 'addr_test1qrv7wa7pj64zthr93ysq6vux5d0spwq0lhfm3wkql77nedxutyh2y8w6cyp6xkwy3fc4zn2p4ceffkc27wz562jx9vyqga7nl9', 279 | amount: '1344798', 280 | assets: [ 281 | { 282 | unit: '57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf391652243484f43', 283 | quantity: '2', 284 | }, 285 | ], 286 | setMax: false, 287 | }, 288 | ], 289 | changeAddress: 290 | 'addr_test1qq43pzxxgfdvffrw6jnrej9840nuylaykv7uzcy56t02xv8m2tfs2k368ger3n3pngluz0lympuh65rzarw5vux862dszv2e9w', 291 | certificates: [], 292 | withdrawals: [], 293 | accountPubKey: 294 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 295 | options: {}, 296 | result: { 297 | totalSpent: '2884577', 298 | fee: '194981', 299 | inputs: UTXO_REAL_SAME_POLICY, 300 | outputs: [ 301 | // external 302 | { 303 | address: 304 | 'addr_test1qrv7wa7pj64zthr93ysq6vux5d0spwq0lhfm3wkql77nedxutyh2y8w6cyp6xkwy3fc4zn2p4ceffkc27wz562jx9vyqga7nl9', 305 | amount: '1344798', 306 | assets: [ 307 | { 308 | unit: '21c3e7f6f954e606fe90017628b048a0067b561a4f6e2aa0e1aa613156616375756d73', 309 | quantity: '2', 310 | }, 311 | ], 312 | setMax: false, 313 | }, 314 | // external 315 | { 316 | address: 317 | 'addr_test1qrv7wa7pj64zthr93ysq6vux5d0spwq0lhfm3wkql77nedxutyh2y8w6cyp6xkwy3fc4zn2p4ceffkc27wz562jx9vyqga7nl9', 318 | amount: '1344798', 319 | assets: [ 320 | { 321 | unit: '57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf391652243484f43', 322 | quantity: '2', 323 | }, 324 | ], 325 | setMax: false, 326 | }, 327 | // change 328 | { 329 | isChange: true, 330 | amount: '699786441', 331 | address: 332 | 'addr_test1qq43pzxxgfdvffrw6jnrej9840nuylaykv7uzcy56t02xv8m2tfs2k368ger3n3pngluz0lympuh65rzarw5vux862dszv2e9w', 333 | assets: [ 334 | { 335 | quantity: '98', 336 | unit: '21c3e7f6f954e606fe90017628b048a0067b561a4f6e2aa0e1aa613156616375756d73', 337 | }, 338 | { 339 | quantity: '4998', 340 | unit: '57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf391652243484f43', 341 | }, 342 | { 343 | quantity: '10000', 344 | unit: '57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf39165224d494e54', 345 | }, 346 | { 347 | quantity: '20000', 348 | unit: '57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf391652256414e494c', 349 | }, 350 | { 351 | quantity: '1000000', 352 | unit: '57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf3916522534245525259', 353 | }, 354 | { 355 | quantity: '1', 356 | unit: '6b8d07d69639e9413dd637a1a815a7323c69c86abbafb66dbfdb1aa7', 357 | }, 358 | { 359 | quantity: '100000', 360 | unit: '769c4c6e9bc3ba5406b9b89fb7beb6819e638ff2e2de63f008d5bcff744e45574d', 361 | }, 362 | ], 363 | }, 364 | ], 365 | }, 366 | }, 367 | ]; 368 | 369 | export const exceptions = [ 370 | { 371 | description: 'Not enough utxos to cover an output amount', 372 | utxos: [utxo1], 373 | outputs: [ 374 | { 375 | address: 376 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 377 | amount: '10000000', 378 | assets: [], 379 | setMax: false, 380 | }, 381 | ], 382 | changeAddress: changeAddress, 383 | certificates: [], 384 | withdrawals: [], 385 | accountPubKey: 386 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 387 | options: {}, 388 | result: 'UTXO_BALANCE_INSUFFICIENT', 389 | }, 390 | { 391 | description: 'Number of utxos is less than the number of outputs', 392 | utxos: [utxo1], 393 | outputs: [ 394 | { 395 | address: 396 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 397 | amount: '10000000', 398 | assets: [], 399 | setMax: false, 400 | }, 401 | { 402 | address: 403 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 404 | amount: '20000000', 405 | assets: [], 406 | setMax: false, 407 | }, 408 | ], 409 | changeAddress: changeAddress, 410 | certificates: [], 411 | withdrawals: [], 412 | accountPubKey: 413 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 414 | options: {}, 415 | result: 'UTXO_NOT_FRAGMENTED_ENOUGH', 416 | }, 417 | { 418 | description: 419 | 'Not enough utxos to cover mandatory change output (multi asset utxo)', 420 | utxos: [utxo2], 421 | outputs: [ 422 | { 423 | address: 424 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 425 | amount: '4800000', 426 | assets: [], 427 | setMax: false, 428 | }, 429 | ], 430 | changeAddress: changeAddress, 431 | certificates: [], 432 | withdrawals: [], 433 | accountPubKey: 434 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 435 | options: {}, 436 | result: 'UTXO_BALANCE_INSUFFICIENT', 437 | }, 438 | ]; 439 | -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- 1 | import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs'; 2 | import { 3 | CARDANO_PARAMS, 4 | CertificateType, 5 | DATA_COST_PER_UTXO_BYTE, 6 | ERROR, 7 | MAX_TOKENS_PER_OUTPUT, 8 | } from '../constants'; 9 | import { 10 | Certificate, 11 | Output, 12 | Utxo, 13 | Withdrawal, 14 | OutputCost, 15 | UserOutput, 16 | Asset, 17 | ChangeOutput, 18 | CardanoDRepType, 19 | } from '../types/types'; 20 | import { CoinSelectionError } from './errors'; 21 | 22 | export const bigNumFromStr = (num: string): CardanoWasm.BigNum => 23 | CardanoWasm.BigNum.from_str(num); 24 | 25 | export const getProtocolMagic = ( 26 | tesnet?: boolean, 27 | ): 28 | | (typeof CARDANO_PARAMS.PROTOCOL_MAGICS)['mainnet'] 29 | | (typeof CARDANO_PARAMS.PROTOCOL_MAGICS)['testnet_preview'] 30 | | (typeof CARDANO_PARAMS.PROTOCOL_MAGICS)['testnet_preprod'] => 31 | tesnet 32 | ? CARDANO_PARAMS.PROTOCOL_MAGICS.testnet_preview 33 | : CARDANO_PARAMS.PROTOCOL_MAGICS.mainnet; 34 | 35 | export const getNetworkId = ( 36 | testnet?: boolean, 37 | ): 38 | | (typeof CARDANO_PARAMS.NETWORK_IDS)['mainnet'] 39 | | (typeof CARDANO_PARAMS.NETWORK_IDS)['testnet_preprod'] 40 | | (typeof CARDANO_PARAMS.NETWORK_IDS)['testnet_preview'] => 41 | testnet 42 | ? CARDANO_PARAMS.NETWORK_IDS.testnet_preview 43 | : CARDANO_PARAMS.NETWORK_IDS.mainnet; 44 | 45 | export const parseAsset = ( 46 | hex: string, 47 | ): { 48 | policyId: string; 49 | assetNameInHex: string; 50 | } => { 51 | const policyIdSize = 56; 52 | const policyId = hex.slice(0, policyIdSize); 53 | const assetNameInHex = hex.slice(policyIdSize); 54 | return { 55 | policyId, 56 | assetNameInHex, 57 | }; 58 | }; 59 | 60 | export const buildMultiAsset = (assets: Asset[]): CardanoWasm.MultiAsset => { 61 | const multiAsset = CardanoWasm.MultiAsset.new(); 62 | const assetsGroupedByPolicy: { 63 | [policyId: string]: CardanoWasm.Assets; 64 | } = {}; 65 | assets.forEach(assetEntry => { 66 | const { policyId, assetNameInHex } = parseAsset(assetEntry.unit); 67 | if (!assetsGroupedByPolicy[policyId]) { 68 | assetsGroupedByPolicy[policyId] = CardanoWasm.Assets.new(); 69 | } 70 | const assets = assetsGroupedByPolicy[policyId]; 71 | assets.insert( 72 | CardanoWasm.AssetName.new(Buffer.from(assetNameInHex, 'hex')), 73 | bigNumFromStr(assetEntry.quantity || '0'), // fallback for an empty string 74 | ); 75 | }); 76 | 77 | Object.keys(assetsGroupedByPolicy).forEach(policyId => { 78 | const scriptHash = CardanoWasm.ScriptHash.from_bytes( 79 | Buffer.from(policyId, 'hex'), 80 | ); 81 | multiAsset.insert(scriptHash, assetsGroupedByPolicy[policyId]); 82 | }); 83 | return multiAsset; 84 | }; 85 | 86 | export const multiAssetToArray = ( 87 | multiAsset: CardanoWasm.MultiAsset | undefined, 88 | ): Asset[] => { 89 | if (!multiAsset) return []; 90 | const assetsArray: Asset[] = []; 91 | const policyHashes = multiAsset.keys(); 92 | 93 | for (let i = 0; i < policyHashes.len(); i++) { 94 | const policyId = policyHashes.get(i); 95 | const assetsInPolicy = multiAsset.get(policyId); 96 | if (!assetsInPolicy) continue; 97 | 98 | const assetNames = assetsInPolicy.keys(); 99 | for (let j = 0; j < assetNames.len(); j++) { 100 | const assetName = assetNames.get(j); 101 | const amount = assetsInPolicy.get(assetName); 102 | if (!amount) continue; 103 | 104 | const policyIdHex = Buffer.from(policyId.to_bytes()).toString('hex'); 105 | const assetNameHex = Buffer.from(assetName.name()).toString('hex'); 106 | 107 | assetsArray.push({ 108 | quantity: amount.to_str(), 109 | unit: `${policyIdHex}${assetNameHex}`, 110 | }); 111 | } 112 | } 113 | return assetsArray; 114 | }; 115 | 116 | export const getAssetAmount = ( 117 | obj: Pick, 118 | asset = 'lovelace', 119 | ): string => obj.amount.find(a => a.unit === asset)?.quantity ?? '0'; 120 | 121 | export const getUtxoQuantity = ( 122 | utxos: Utxo[], 123 | asset = 'lovelace', 124 | ): CardanoWasm.BigNum => 125 | utxos.reduce( 126 | (acc, utxo) => acc.checked_add(bigNumFromStr(getAssetAmount(utxo, asset))), 127 | bigNumFromStr('0'), 128 | ); 129 | 130 | export const getOutputQuantity = ( 131 | outputs: Output[], 132 | asset = 'lovelace', 133 | ): CardanoWasm.BigNum => { 134 | if (asset === 'lovelace') { 135 | return outputs.reduce( 136 | (acc, output) => acc.checked_add(bigNumFromStr(output.amount ?? '0')), 137 | bigNumFromStr('0'), 138 | ); 139 | } 140 | return outputs.reduce( 141 | (acc, output) => 142 | acc.checked_add( 143 | bigNumFromStr( 144 | output.assets?.find(a => a.unit === asset)?.quantity ?? '0', 145 | ), 146 | ), 147 | bigNumFromStr('0'), 148 | ); 149 | }; 150 | 151 | export const sortUtxos = (utxos: Utxo[], asset = 'lovelace'): Utxo[] => { 152 | const copy: Utxo[] = JSON.parse(JSON.stringify(utxos)); 153 | return copy.sort((u1, u2) => 154 | bigNumFromStr(getAssetAmount(u2, asset)).compare( 155 | bigNumFromStr(getAssetAmount(u1, asset)), 156 | ), 157 | ); 158 | }; 159 | 160 | export const buildTxInput = ( 161 | utxo: Utxo, 162 | ): { 163 | input: CardanoWasm.TransactionInput; 164 | address: CardanoWasm.Address; 165 | amount: CardanoWasm.Value; 166 | } => { 167 | const input = CardanoWasm.TransactionInput.new( 168 | CardanoWasm.TransactionHash.from_bytes(Buffer.from(utxo.txHash, 'hex')), 169 | utxo.outputIndex, 170 | ); 171 | 172 | const amount = CardanoWasm.Value.new(bigNumFromStr(getAssetAmount(utxo))); 173 | const assets = utxo.amount.filter(a => a.unit !== 'lovelace'); 174 | if (assets.length > 0) { 175 | const multiAsset = buildMultiAsset(assets); 176 | amount.set_multiasset(multiAsset); 177 | } 178 | 179 | const address = CardanoWasm.Address.from_bech32(utxo.address); 180 | 181 | return { input, address, amount }; 182 | }; 183 | 184 | export const buildTxOutput = ( 185 | output: Output, 186 | dummyAddress: string, 187 | ): CardanoWasm.TransactionOutput => { 188 | // If output.address was not defined fallback to bech32 address (useful for "precompose" tx 189 | // which doesn't have all necessary data, but we can fill in the blanks and return some info such as fee) 190 | const outputAddr = 191 | output.address && CardanoWasm.ByronAddress.is_valid(output.address) 192 | ? CardanoWasm.ByronAddress.from_base58(output.address).to_address() 193 | : CardanoWasm.Address.from_bech32(output.address ?? dummyAddress); 194 | 195 | // Set initial amount 196 | const outputAmount = output.amount 197 | ? bigNumFromStr(output.amount) 198 | : bigNumFromStr('0'); 199 | 200 | // Create Value including assets 201 | let outputValue = CardanoWasm.Value.new(outputAmount); 202 | const multiAsset = 203 | output.assets.length > 0 ? buildMultiAsset(output.assets) : null; 204 | if (multiAsset) { 205 | outputValue.set_multiasset(multiAsset); 206 | } 207 | 208 | // Calculate min required ADA for the output 209 | let txOutput = CardanoWasm.TransactionOutput.new(outputAddr, outputValue); 210 | const minAdaRequired = CardanoWasm.min_ada_for_output( 211 | txOutput, 212 | DATA_COST_PER_UTXO_BYTE, 213 | ); 214 | 215 | // If calculated min required ada is greater than current output value than adjust it 216 | if (outputAmount.compare(minAdaRequired) < 0) { 217 | outputValue = CardanoWasm.Value.new(minAdaRequired); 218 | if (multiAsset) { 219 | outputValue.set_multiasset(multiAsset); 220 | } 221 | txOutput = CardanoWasm.TransactionOutput.new(outputAddr, outputValue); 222 | } 223 | 224 | return txOutput; 225 | }; 226 | 227 | export const getOutputCost = ( 228 | txBuilder: CardanoWasm.TransactionBuilder, 229 | output: Output, 230 | dummyAddress: string, 231 | ): OutputCost => { 232 | const txOutput = buildTxOutput(output, dummyAddress); 233 | const outputFee = txBuilder.fee_for_output(txOutput); 234 | const minAda = CardanoWasm.min_ada_for_output( 235 | txOutput, 236 | DATA_COST_PER_UTXO_BYTE, 237 | ); 238 | 239 | return { 240 | output: txOutput, 241 | outputFee, 242 | minOutputAmount: minAda, // should match https://cardano-ledger.readthedocs.io/en/latest/explanations/min-utxo.html 243 | }; 244 | }; 245 | 246 | export const prepareWithdrawals = ( 247 | withdrawals: Withdrawal[], 248 | ): CardanoWasm.Withdrawals => { 249 | const preparedWithdrawals = CardanoWasm.Withdrawals.new(); 250 | 251 | withdrawals.forEach(withdrawal => { 252 | const rewardAddress = CardanoWasm.RewardAddress.from_address( 253 | CardanoWasm.Address.from_bech32(withdrawal.stakeAddress), 254 | ); 255 | 256 | if (rewardAddress) { 257 | preparedWithdrawals.insert( 258 | rewardAddress, 259 | bigNumFromStr(withdrawal.amount), 260 | ); 261 | } 262 | }); 263 | 264 | return preparedWithdrawals; 265 | }; 266 | 267 | export const prepareCertificates = ( 268 | certificates: Certificate[], 269 | accountKey: CardanoWasm.Bip32PublicKey, 270 | ): CardanoWasm.Certificates => { 271 | const preparedCertificates = CardanoWasm.Certificates.new(); 272 | if (certificates.length === 0) return preparedCertificates; 273 | 274 | const stakeKey = accountKey.derive(2).derive(0); 275 | const stakeCred = CardanoWasm.Credential.from_keyhash( 276 | stakeKey.to_raw_key().hash(), 277 | ); 278 | 279 | certificates.forEach(cert => { 280 | if (cert.type === CertificateType.STAKE_REGISTRATION) { 281 | preparedCertificates.add( 282 | CardanoWasm.Certificate.new_stake_registration( 283 | CardanoWasm.StakeRegistration.new(stakeCred), 284 | ), 285 | ); 286 | } else if (cert.type === CertificateType.STAKE_DELEGATION) { 287 | preparedCertificates.add( 288 | CardanoWasm.Certificate.new_stake_delegation( 289 | CardanoWasm.StakeDelegation.new( 290 | stakeCred, 291 | CardanoWasm.Ed25519KeyHash.from_bytes( 292 | Buffer.from(cert.pool, 'hex'), 293 | ), 294 | ), 295 | ), 296 | ); 297 | } else if (cert.type === CertificateType.STAKE_DEREGISTRATION) { 298 | preparedCertificates.add( 299 | CardanoWasm.Certificate.new_stake_deregistration( 300 | CardanoWasm.StakeDeregistration.new(stakeCred), 301 | ), 302 | ); 303 | } else if (cert.type === CertificateType.VOTE_DELEGATION) { 304 | let targetDRep: CardanoWasm.DRep; 305 | switch (cert.dRep.type) { 306 | case CardanoDRepType.ABSTAIN: 307 | targetDRep = CardanoWasm.DRep.new_always_abstain(); 308 | break; 309 | case CardanoDRepType.NO_CONFIDENCE: 310 | targetDRep = CardanoWasm.DRep.new_always_no_confidence(); 311 | break; 312 | case CardanoDRepType.KEY_HASH: 313 | targetDRep = CardanoWasm.DRep.new_key_hash( 314 | CardanoWasm.Ed25519KeyHash.from_hex(cert.dRep.keyHash), 315 | ); 316 | break; 317 | case CardanoDRepType.SCRIPT_HASH: 318 | targetDRep = CardanoWasm.DRep.new_script_hash( 319 | CardanoWasm.ScriptHash.from_hex(cert.dRep.scriptHash), 320 | ); 321 | break; 322 | } 323 | 324 | if (targetDRep) { 325 | preparedCertificates.add( 326 | CardanoWasm.Certificate.new_vote_delegation( 327 | CardanoWasm.VoteDelegation.new(stakeCred, targetDRep), 328 | ), 329 | ); 330 | } 331 | } else { 332 | throw new CoinSelectionError(ERROR.UNSUPPORTED_CERTIFICATE_TYPE); 333 | } 334 | }); 335 | return preparedCertificates; 336 | }; 337 | 338 | export const calculateRequiredDeposit = ( 339 | certificates: Certificate[], 340 | ): number => { 341 | const CertificateDeposit = { 342 | [CertificateType.STAKE_DELEGATION]: 0, 343 | [CertificateType.VOTE_DELEGATION]: 0, 344 | [CertificateType.STAKE_POOL_REGISTRATION]: 500000000, 345 | [CertificateType.STAKE_REGISTRATION]: 2000000, 346 | [CertificateType.STAKE_DEREGISTRATION]: -2000000, 347 | } as const; 348 | return certificates.reduce( 349 | (acc, cert) => (acc += CertificateDeposit[cert.type]), 350 | 0, 351 | ); 352 | }; 353 | 354 | export const setMinUtxoValueForOutputs = ( 355 | txBuilder: CardanoWasm.TransactionBuilder, 356 | outputs: UserOutput[], 357 | dummyAddress: string, 358 | ): UserOutput[] => { 359 | const preparedOutputs = outputs.map(output => { 360 | // sets minimal output ADA amount in case of multi-asset output 361 | const { minOutputAmount } = getOutputCost(txBuilder, output, dummyAddress); 362 | const outputAmount = bigNumFromStr(output.amount || '0'); 363 | 364 | let amount: string | undefined; 365 | if (output.assets.length > 0 && outputAmount.compare(minOutputAmount) < 0) { 366 | // output with an asset(s) adjust minimum ADA to met network requirements 367 | amount = minOutputAmount.to_str(); 368 | } else { 369 | amount = output.amount; 370 | } 371 | 372 | if ( 373 | !output.setMax && 374 | output.assets.length === 0 && 375 | output.amount && 376 | outputAmount.compare(minOutputAmount) < 0 377 | ) { 378 | // Case of an output without any asset, and without setMax = true 379 | // If the user entered less than min utxo val then throw an error (won't throw if there is no amount yet) 380 | // (On outputs with setMax flag we set '0' on purpose) 381 | // (On outputs with an asset we automatically adjust ADA amount if it is below required minimum) 382 | throw new CoinSelectionError(ERROR.UTXO_VALUE_TOO_SMALL); 383 | } 384 | 385 | if (output.setMax) { 386 | // if setMax is active set initial value to 0 387 | if (output.assets.length > 0) { 388 | output.assets[0].quantity = '0'; 389 | } else { 390 | amount = '0'; 391 | } 392 | } 393 | 394 | return { 395 | ...output, 396 | // if output contains assets make sure that minUtxoValue is at least minOutputAmount (even for output where we want to setMax) 397 | amount, 398 | } as UserOutput; 399 | }); 400 | return preparedOutputs; 401 | }; 402 | 403 | export const splitChangeOutput = ( 404 | txBuilder: CardanoWasm.TransactionBuilder, 405 | singleChangeOutput: OutputCost, 406 | changeAddress: string, 407 | maxTokensPerOutput = MAX_TOKENS_PER_OUTPUT, 408 | ): OutputCost[] => { 409 | // TODO: https://github.com/Emurgo/cardano-serialization-lib/pull/236 410 | const multiAsset = singleChangeOutput.output.amount().multiasset(); 411 | if (!multiAsset || (multiAsset && multiAsset.len() < maxTokensPerOutput)) { 412 | return [singleChangeOutput]; 413 | } 414 | 415 | let lovelaceAvailable = singleChangeOutput.output 416 | .amount() 417 | .coin() 418 | .checked_add(singleChangeOutput.outputFee); 419 | 420 | const allAssets = multiAssetToArray( 421 | singleChangeOutput.output.amount().multiasset(), 422 | ); 423 | const nAssetBundles = Math.ceil(allAssets.length / maxTokensPerOutput); 424 | 425 | const changeOutputs: ChangeOutput[] = []; 426 | // split change output to multiple outputs, where each bundle has maximum of maxTokensPerOutput assets 427 | for (let i = 0; i < nAssetBundles; i++) { 428 | const assetsBundle = allAssets.slice( 429 | i * maxTokensPerOutput, 430 | (i + 1) * maxTokensPerOutput, 431 | ); 432 | 433 | const outputValue = CardanoWasm.Value.new_from_assets( 434 | buildMultiAsset(assetsBundle), 435 | ); 436 | const txOutput = CardanoWasm.TransactionOutput.new( 437 | CardanoWasm.Address.from_bech32(changeAddress), 438 | outputValue, 439 | ); 440 | 441 | const minAdaRequired = CardanoWasm.min_ada_for_output( 442 | txOutput, 443 | DATA_COST_PER_UTXO_BYTE, 444 | ); 445 | 446 | changeOutputs.push({ 447 | isChange: true, 448 | address: changeAddress, 449 | amount: minAdaRequired.to_str(), 450 | assets: assetsBundle, 451 | }); 452 | } 453 | 454 | const changeOutputsCost = changeOutputs.map((partialChange, i) => { 455 | let changeOutputCost = getOutputCost( 456 | txBuilder, 457 | partialChange, 458 | changeAddress, 459 | ); 460 | lovelaceAvailable = lovelaceAvailable.clamped_sub( 461 | bigNumFromStr(partialChange.amount).checked_add( 462 | changeOutputCost.outputFee, 463 | ), 464 | ); 465 | 466 | if (i === changeOutputs.length - 1) { 467 | // add all unused ADA to the last change output 468 | let changeOutputAmount = lovelaceAvailable.checked_add( 469 | bigNumFromStr(partialChange.amount), 470 | ); 471 | 472 | if (changeOutputAmount.compare(changeOutputCost.minOutputAmount) < 0) { 473 | // computed change amount would be below minUtxoValue 474 | // set change output amount to met minimum requirements for minUtxoValue 475 | changeOutputAmount = changeOutputCost.minOutputAmount; 476 | } 477 | partialChange.amount = changeOutputAmount.to_str(); 478 | changeOutputCost = getOutputCost(txBuilder, partialChange, changeAddress); 479 | } 480 | return changeOutputCost; 481 | }); 482 | 483 | return changeOutputsCost; 484 | }; 485 | 486 | export const prepareChangeOutput = ( 487 | txBuilder: CardanoWasm.TransactionBuilder, 488 | usedUtxos: Utxo[], 489 | preparedOutputs: Output[], 490 | changeAddress: string, 491 | utxosTotalAmount: CardanoWasm.BigNum, 492 | totalOutputAmount: CardanoWasm.BigNum, 493 | totalFeesAmount: CardanoWasm.BigNum, 494 | pickAdditionalUtxo?: () => ReturnType, 495 | ): OutputCost | null => { 496 | // change output amount should be lowered by the cost of the change output (fee + minUtxoVal) 497 | // The cost will be subtracted once we calculate it. 498 | const placeholderChangeOutputAmount = utxosTotalAmount.clamped_sub( 499 | totalFeesAmount.checked_add(totalOutputAmount), 500 | ); 501 | const uniqueAssets: string[] = []; 502 | usedUtxos.forEach(utxo => { 503 | const assets = utxo.amount.filter(a => a.unit !== 'lovelace'); 504 | assets.forEach(asset => { 505 | if (!uniqueAssets.includes(asset.unit)) { 506 | uniqueAssets.push(asset.unit); 507 | } 508 | }); 509 | }); 510 | 511 | const changeOutputAssets = uniqueAssets 512 | .map(assetUnit => { 513 | const assetInputAmount = getUtxoQuantity(usedUtxos, assetUnit); 514 | const assetSpentAmount = getOutputQuantity(preparedOutputs, assetUnit); 515 | return { 516 | unit: assetUnit, 517 | quantity: assetInputAmount.clamped_sub(assetSpentAmount).to_str(), 518 | }; 519 | }) 520 | .filter(asset => asset.quantity !== '0'); 521 | 522 | const changeOutputCost = getOutputCost( 523 | txBuilder, 524 | { 525 | address: changeAddress, 526 | amount: placeholderChangeOutputAmount.to_str(), 527 | assets: changeOutputAssets, 528 | }, 529 | changeAddress, 530 | ); 531 | 532 | // calculate change output amount as utxosTotalAmount - totalOutputAmount - totalFeesAmount - change output fee 533 | const totalSpent = totalOutputAmount 534 | .checked_add(totalFeesAmount) 535 | .checked_add(changeOutputCost.outputFee); 536 | let changeOutputAmount = utxosTotalAmount.clamped_sub(totalSpent); 537 | 538 | // Sum of all tokens in utxos must be same as sum of the tokens in external + change outputs 539 | // If computed change output doesn't contain any tokens then it makes sense to add it only if the fee + minUtxoValue is less then the amount 540 | let isChangeOutputNeeded = false; 541 | if ( 542 | changeOutputAssets.length > 0 || 543 | changeOutputAmount.compare(changeOutputCost.minOutputAmount) >= 0 544 | ) { 545 | isChangeOutputNeeded = true; 546 | } else if ( 547 | pickAdditionalUtxo && 548 | changeOutputAmount.compare(bigNumFromStr('5000')) >= 0 549 | ) { 550 | // change amount is above our constant (0.005 ADA), but still less than required minUtxoValue 551 | // try to add another utxo recalculate change again 552 | const utxo = pickAdditionalUtxo(); 553 | if (utxo) { 554 | utxo.addUtxo(); 555 | const newTotalFee = txBuilder.min_fee(); 556 | return prepareChangeOutput( 557 | txBuilder, 558 | usedUtxos, 559 | preparedOutputs, 560 | changeAddress, 561 | getUtxoQuantity(usedUtxos, 'lovelace'), 562 | totalOutputAmount, 563 | newTotalFee, 564 | pickAdditionalUtxo, 565 | ); 566 | } 567 | } 568 | 569 | if (isChangeOutputNeeded) { 570 | if (changeOutputAmount.compare(changeOutputCost.minOutputAmount) < 0) { 571 | // computed change amount would be below minUtxoValue 572 | // set change output amount to met minimum requirements for minUtxoValue 573 | changeOutputAmount = changeOutputCost.minOutputAmount; 574 | } 575 | 576 | // TODO: changeOutputCost.output.amount().set_coin(changeOutputAmount)? 577 | const txOutput = buildTxOutput( 578 | { 579 | amount: changeOutputAmount.to_str(), 580 | address: changeAddress, 581 | assets: changeOutputAssets, 582 | }, 583 | changeAddress, 584 | ); 585 | 586 | // WARNING: It returns a change output also in a case where we don't have enough utxos to cover the output cost, but the change output is needed because it contains additional assets 587 | return { 588 | outputFee: changeOutputCost.outputFee, 589 | minOutputAmount: changeOutputCost.minOutputAmount, 590 | output: txOutput, 591 | }; 592 | } 593 | // Change output not needed 594 | return null; 595 | }; 596 | 597 | export const getTxBuilder = (a = '44'): CardanoWasm.TransactionBuilder => 598 | CardanoWasm.TransactionBuilder.new( 599 | CardanoWasm.TransactionBuilderConfigBuilder.new() 600 | .fee_algo( 601 | CardanoWasm.LinearFee.new(bigNumFromStr(a), bigNumFromStr('155381')), 602 | ) 603 | .pool_deposit(bigNumFromStr('500000000')) 604 | .key_deposit(bigNumFromStr('2000000')) 605 | .coins_per_utxo_byte(bigNumFromStr(CARDANO_PARAMS.COINS_PER_UTXO_BYTE)) 606 | .max_value_size(CARDANO_PARAMS.MAX_VALUE_SIZE) 607 | .max_tx_size(CARDANO_PARAMS.MAX_TX_SIZE) 608 | .build(), 609 | ); 610 | 611 | export const getUnsatisfiedAssets = ( 612 | selectedUtxos: Utxo[], 613 | outputs: Output[], 614 | ): string[] => { 615 | const assets: string[] = []; 616 | 617 | outputs.forEach(output => { 618 | if (output.assets.length > 0) { 619 | const asset = output.assets[0]; 620 | const assetAmountInUtxos = getUtxoQuantity(selectedUtxos, asset.unit); 621 | if (assetAmountInUtxos.compare(bigNumFromStr(asset.quantity)) < 0) { 622 | assets.push(asset.unit); 623 | } 624 | } 625 | }); 626 | 627 | const lovelaceUtxo = getUtxoQuantity(selectedUtxos, 'lovelace'); 628 | if (lovelaceUtxo.compare(getOutputQuantity(outputs, 'lovelace')) < 0) { 629 | assets.push('lovelace'); 630 | } 631 | 632 | return assets; 633 | }; 634 | 635 | export const getInitialUtxoSet = ( 636 | utxos: Utxo[], 637 | maxOutput: UserOutput | undefined, 638 | ): { 639 | used: Utxo[]; 640 | remaining: Utxo[]; 641 | } => { 642 | // Picks all utxos containing an asset on which the user requested to set maximum value 643 | if (!maxOutput) 644 | return { 645 | used: [], 646 | remaining: utxos, 647 | }; 648 | 649 | const used: Utxo[] = []; 650 | const remaining: Utxo[] = []; 651 | 652 | const maxOutputAsset = maxOutput.assets[0]?.unit ?? 'lovelace'; 653 | // either all UTXOs will be used (send max for ADA output) or initial set of used utxos will contain all utxos containing given token 654 | utxos.forEach(u => { 655 | if (u.amount.find(a => a.unit === maxOutputAsset)) { 656 | used.push(u); 657 | } else { 658 | remaining.push(u); 659 | } 660 | }); 661 | return { 662 | used, 663 | remaining, 664 | }; 665 | }; 666 | 667 | export const setMaxOutput = ( 668 | txBuilder: CardanoWasm.TransactionBuilder, 669 | maxOutput: UserOutput, 670 | changeOutput: OutputCost | null, 671 | ): { 672 | maxOutput: UserOutput; 673 | } => { 674 | const maxOutputAsset = maxOutput.assets[0]?.unit ?? 'lovelace'; 675 | let newMaxAmount = bigNumFromStr('0'); 676 | 677 | const changeOutputAssets = multiAssetToArray( 678 | changeOutput?.output.amount().multiasset(), 679 | ); 680 | 681 | if (maxOutputAsset === 'lovelace') { 682 | // set maxOutput for ADA 683 | if (changeOutput) { 684 | // Calculate the cost of previous dummy set-max output 685 | const previousMaxOutputCost = getOutputCost( 686 | txBuilder, 687 | maxOutput, 688 | maxOutput.address ?? changeOutput.output.address().to_bech32(), 689 | ); 690 | newMaxAmount = changeOutput.output.amount().coin(); 691 | 692 | if (changeOutputAssets.length === 0) { 693 | // Add a fee that was previously consumed by the dummy max output. 694 | // Cost calculated for the change output will be greater (due to larger coin amount 695 | // than in dummy output - which is 0) than the cost of the dummy set-max output. 696 | newMaxAmount = newMaxAmount.checked_add( 697 | previousMaxOutputCost.outputFee, 698 | ); 699 | changeOutput = null; 700 | } else { 701 | newMaxAmount = newMaxAmount.clamped_sub(changeOutput.minOutputAmount); 702 | 703 | const txOutput = CardanoWasm.TransactionOutput.new( 704 | changeOutput.output.address(), 705 | CardanoWasm.Value.new(newMaxAmount), 706 | ); 707 | const minUtxoVal = CardanoWasm.min_ada_for_output( 708 | txOutput, 709 | DATA_COST_PER_UTXO_BYTE, 710 | ); 711 | 712 | if (newMaxAmount.compare(minUtxoVal) < 0) { 713 | // the amount would be less than min required ADA 714 | throw new CoinSelectionError(ERROR.UTXO_BALANCE_INSUFFICIENT); 715 | } 716 | } 717 | } 718 | maxOutput.amount = newMaxAmount.to_str(); 719 | } else { 720 | // set maxOutput for token 721 | if (changeOutput) { 722 | // max amount of the asset in output is equal to its quantity in change output 723 | newMaxAmount = bigNumFromStr( 724 | changeOutputAssets.find(a => a.unit === maxOutputAsset)?.quantity ?? 725 | '0', 726 | ); 727 | maxOutput.assets[0].quantity = newMaxAmount.to_str(); // TODO: set 0 if no change? 728 | 729 | const txOutput = CardanoWasm.TransactionOutput.new( 730 | changeOutput.output.address(), 731 | // new_from_assets does not automatically include required ADA 732 | CardanoWasm.Value.new_from_assets(buildMultiAsset(maxOutput.assets)), 733 | ); 734 | 735 | // adjust ADA amount to cover min ada for the asset 736 | maxOutput.amount = CardanoWasm.min_ada_for_output( 737 | txOutput, 738 | DATA_COST_PER_UTXO_BYTE, 739 | ).to_str(); 740 | } 741 | } 742 | 743 | return { maxOutput }; 744 | }; 745 | 746 | export const getUserOutputQuantityWithDeposit = ( 747 | outputs: UserOutput[], 748 | deposit: number, 749 | asset = 'lovelace', 750 | ): CardanoWasm.BigNum => { 751 | let amount = getOutputQuantity(outputs, asset); 752 | if (deposit > 0) { 753 | amount = amount.checked_add(bigNumFromStr(deposit.toString())); 754 | } 755 | return amount; 756 | }; 757 | 758 | export const filterUtxos = (utxos: Utxo[], asset: string): Utxo[] => { 759 | return utxos.filter(utxo => utxo.amount.find(a => a.unit === asset)); 760 | }; 761 | 762 | export const getRandomUtxo = ( 763 | txBuilder: CardanoWasm.TransactionBuilder, 764 | utxoRemaining: Utxo[], 765 | utxoSelected: Utxo[], 766 | ): { 767 | utxo: Utxo; 768 | addUtxo: () => void; 769 | } | null => { 770 | const index = Math.floor(Math.random() * utxoRemaining.length); 771 | const utxo = utxoRemaining[index]; 772 | 773 | if (!utxo) return null; 774 | return { 775 | utxo, 776 | addUtxo: () => { 777 | utxoSelected.push(utxo); 778 | const { input, address, amount } = buildTxInput(utxo); 779 | txBuilder.add_regular_input(address, input, amount); 780 | utxoRemaining.splice(utxoRemaining.indexOf(utxo), 1); 781 | }, 782 | }; 783 | }; 784 | 785 | export const calculateUserOutputsFee = ( 786 | txBuilder: CardanoWasm.TransactionBuilder, 787 | userOutputs: UserOutput[], 788 | changeAddress: string, 789 | ) => { 790 | // Calculate fee and minUtxoValue for all external outputs 791 | const outputsCost = userOutputs.map(output => 792 | getOutputCost(txBuilder, output, changeAddress), 793 | ); 794 | 795 | const totalOutputsFee = outputsCost.reduce( 796 | (acc, output) => (acc = acc.checked_add(output.outputFee)), 797 | bigNumFromStr('0'), 798 | ); 799 | 800 | return totalOutputsFee; 801 | }; 802 | 803 | export const orderInputs = ( 804 | inputsToOrder: Utxo[], 805 | txBody: CardanoWasm.TransactionBody, 806 | ): Utxo[] => { 807 | // reorder inputs to match order within tx 808 | const orderedInputs: Utxo[] = []; 809 | for (let i = 0; i < txBody.inputs().len(); i++) { 810 | const txid = Buffer.from( 811 | txBody.inputs().get(i).transaction_id().to_bytes(), 812 | ).toString('hex'); 813 | const outputIndex = txBody.inputs().get(i).index(); 814 | const utxo = inputsToOrder.find( 815 | uu => uu.txHash === txid && uu.outputIndex === outputIndex, 816 | ); 817 | if (!utxo) { 818 | throw new Error( 819 | 'Failed to order the utxos to match the order of inputs in constructed tx. THIS SHOULD NOT HAPPEN', 820 | ); 821 | } 822 | orderedInputs.push(utxo); 823 | } 824 | return orderedInputs; 825 | }; 826 | -------------------------------------------------------------------------------- /tests/methods/fixtures/largestFirst.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CardanoDRepType, 3 | Certificate, 4 | CertificateVoteDelegation, 5 | } from '../../../src/types/types'; 6 | import { 7 | changeAddress, 8 | setMaxAdaInputs, 9 | utxo1, 10 | utxo2, 11 | utxo3, 12 | utxo4, 13 | utxo5, 14 | utxo6, 15 | utxo7, 16 | utxo8, 17 | } from '../../fixtures/constants'; 18 | 19 | export const nonFinalCompose = [ 20 | { 21 | description: 'Non-final compose: amount not filled', 22 | utxos: [utxo1], 23 | outputs: [ 24 | { 25 | address: 26 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 27 | amount: undefined, 28 | assets: [], 29 | setMax: false, 30 | }, 31 | ], 32 | changeAddress: changeAddress, 33 | certificates: [], 34 | withdrawals: [], 35 | accountPubKey: 36 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 37 | options: {}, 38 | result: { 39 | totalSpent: '168317', 40 | fee: '168317', 41 | }, 42 | }, 43 | { 44 | description: 'Non-final compose: address not filled', 45 | utxos: [utxo1], 46 | outputs: [ 47 | { 48 | address: undefined, 49 | amount: '2000000', 50 | assets: [], 51 | setMax: false, 52 | }, 53 | ], 54 | changeAddress: changeAddress, 55 | certificates: [], 56 | withdrawals: [], 57 | accountPubKey: 58 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 59 | options: {}, 60 | result: { 61 | totalSpent: '2168317', 62 | fee: '168317', 63 | }, 64 | }, 65 | { 66 | description: 'Non-final compose, 2 outputs, 1 amount not filled', 67 | utxos: [utxo1], 68 | outputs: [ 69 | { 70 | address: 71 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 72 | amount: '2000000', 73 | assets: [], 74 | setMax: false, 75 | }, 76 | { 77 | address: 78 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 79 | amount: undefined, 80 | assets: [], 81 | setMax: false, 82 | }, 83 | ], 84 | changeAddress: changeAddress, 85 | certificates: [], 86 | withdrawals: [], 87 | accountPubKey: 88 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 89 | options: {}, 90 | result: { 91 | totalSpent: '2171177', 92 | fee: '171177', 93 | }, 94 | }, 95 | { 96 | description: 'Non-final compose: token amount not filled', 97 | utxos: [utxo1], 98 | outputs: [ 99 | { 100 | address: 101 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 102 | amount: undefined, 103 | assets: [ 104 | { 105 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 106 | quantity: '0', 107 | }, 108 | ], 109 | setMax: false, 110 | }, 111 | ], 112 | changeAddress: changeAddress, 113 | certificates: [], 114 | withdrawals: [], 115 | accountPubKey: 116 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 117 | options: {}, 118 | result: { 119 | totalSpent: '1307873', 120 | fee: '170033', 121 | }, 122 | }, 123 | ]; 124 | 125 | export const coinSelection = [ 126 | { 127 | description: 'send max ada only utxos', 128 | utxos: setMaxAdaInputs, 129 | outputs: [ 130 | { 131 | address: 132 | 'addr_test1qr9tax9jxzt05y65m8xanngng36mh7hpf23jy53xwyd9y5qj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfms0kcepv', 133 | amount: undefined, 134 | assets: [], 135 | setMax: true, 136 | }, 137 | ], 138 | changeAddress: changeAddress, 139 | certificates: [], 140 | withdrawals: [], 141 | accountPubKey: 142 | 'd507c8f866691bd96e131334c355188b1a1d0b2fa0ab11545075aab332d77d9eb19657ad13ee581b56b0f8d744d66ca356b93d42fe176b3de007d53e9c4c4e7a', 143 | ttl: 66578367, 144 | options: {}, 145 | result: { 146 | totalSpent: '5222414726', 147 | fee: '176985', 148 | tx: { 149 | body: 'a400d9010288825820d6de3f33c3b421167eb1726c48129990ec16512dd829ad2239751ba49773b30c02825820d6de3f33c3b421167eb1726c48129990ec16512dd829ad2239751ba49773b30c05825820d6de3f33c3b421167eb1726c48129990ec16512dd829ad2239751ba49773b30c08825820d6de3f33c3b421167eb1726c48129990ec16512dd829ad2239751ba49773b30c0b825820d6de3f33c3b421167eb1726c48129990ec16512dd829ad2239751ba49773b30c0e825820d6de3f33c3b421167eb1726c48129990ec16512dd829ad2239751ba49773b30c11825820d6de3f33c3b421167eb1726c48129990ec16512dd829ad2239751ba49773b30c15825820d6de3f33c3b421167eb1726c48129990ec16512dd829ad2239751ba49773b30c16018182583900cabe98b23096fa1354d9cdd9cd134475bbfae14aa3225226711a5250122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b42771b000000013745062d021a0002b359031a03f7e7bf', 150 | hash: '7ec409e0d2e14769547cf3911f6d9faf3f7411327926baa26154f39508e6956c', 151 | size: 487, 152 | }, 153 | inputs: setMaxAdaInputs, 154 | outputs: [ 155 | { 156 | address: 157 | 'addr_test1qr9tax9jxzt05y65m8xanngng36mh7hpf23jy53xwyd9y5qj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfms0kcepv', 158 | amount: '5222237741', 159 | assets: [], 160 | }, 161 | ], 162 | }, 163 | }, 164 | { 165 | description: 'send max sundae', 166 | utxos: [ 167 | { 168 | address: 169 | 'addr1qy4xpnf4lk560dgrds5zsunh6xdssg94c5sc8dqdclcn2fdl85agr52j3ffkwzq2yasu59ccwvfj39kel85ng3u7lhlq4e4m4l', 170 | txHash: 171 | '9ed3ef581f545f2143eca490d7f20a511100add747bb3d651cc2aa5815f77b1d', 172 | outputIndex: 1, 173 | amount: [ 174 | { 175 | quantity: '1344974', 176 | unit: 'lovelace', 177 | }, 178 | { 179 | quantity: '5675656536', 180 | unit: '9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d7753554e444145', 181 | }, 182 | ], 183 | }, 184 | { 185 | address: 186 | 'addr1q860vxljhadqxnrrsr2j6yxnwpdkyquq74lmghx502aj0r28d2kd47hsre5v9urjyu8s0ryk38dxzw0t5jesncw4v90sp0878u', 187 | txHash: 188 | '06227a5ee5640d26224470ad195c82941bfa49386a85149c09c465c4edb0edc0', 189 | outputIndex: 0, 190 | amount: [ 191 | { 192 | quantity: '10000000', 193 | unit: 'lovelace', 194 | }, 195 | ], 196 | }, 197 | ], 198 | outputs: [ 199 | { 200 | address: 201 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 202 | amount: '1344798', 203 | assets: [ 204 | { 205 | unit: '9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d7753554e444145', 206 | quantity: '5675656536', 207 | }, 208 | ], 209 | setMax: true, 210 | }, 211 | ], 212 | changeAddress: changeAddress, 213 | certificates: [], 214 | withdrawals: [], 215 | accountPubKey: 216 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 217 | ttl: 66578367, 218 | options: {}, 219 | result: { 220 | totalSpent: '1357705', 221 | fee: '176765', 222 | tx: { 223 | body: 'a400d901028282582006227a5ee5640d26224470ad195c82941bfa49386a85149c09c465c4edb0edc0008258209ed3ef581f545f2143eca490d7f20a511100add747bb3d651cc2aa5815f77b1d010182825839013af9d8434bea8de03cd698d5fa1c6b82b991146a755f509e95d6b53b15ab05b40d24d39c9d14dfec04d87ed071f2c66484b3ab83ab3d603d821a0012050ca1581c9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77a14653554e4441451b00000001524ba55882583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f1a009864c5021a0002b27d031a03f7e7bf', 224 | hash: 'ce3aa8b670058ac029ad30cb572598f4c35ae3e67199b94ec1cf1654127b0dcf', 225 | size: 482, 226 | }, 227 | inputs: [ 228 | { 229 | address: 230 | 'addr1q860vxljhadqxnrrsr2j6yxnwpdkyquq74lmghx502aj0r28d2kd47hsre5v9urjyu8s0ryk38dxzw0t5jesncw4v90sp0878u', 231 | txHash: 232 | '06227a5ee5640d26224470ad195c82941bfa49386a85149c09c465c4edb0edc0', 233 | outputIndex: 0, 234 | amount: [ 235 | { 236 | quantity: '10000000', 237 | unit: 'lovelace', 238 | }, 239 | ], 240 | }, 241 | { 242 | address: 243 | 'addr1qy4xpnf4lk560dgrds5zsunh6xdssg94c5sc8dqdclcn2fdl85agr52j3ffkwzq2yasu59ccwvfj39kel85ng3u7lhlq4e4m4l', 244 | txHash: 245 | '9ed3ef581f545f2143eca490d7f20a511100add747bb3d651cc2aa5815f77b1d', 246 | outputIndex: 1, 247 | amount: [ 248 | { 249 | quantity: '1344974', 250 | unit: 'lovelace', 251 | }, 252 | { 253 | quantity: '5675656536', 254 | unit: '9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d7753554e444145', 255 | }, 256 | ], 257 | }, 258 | ], 259 | outputs: [ 260 | { 261 | address: 262 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 263 | amount: '1180940', 264 | assets: [ 265 | { 266 | quantity: '5675656536', 267 | unit: '9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d7753554e444145', 268 | }, 269 | ], 270 | setMax: true, 271 | }, 272 | { 273 | address: changeAddress, 274 | amount: '9987269', 275 | assets: [], 276 | }, 277 | ], 278 | }, 279 | }, 280 | 281 | { 282 | description: '1 ADA only utxo, 1 output, no change (dust burned as fee)', 283 | utxos: [utxo1], 284 | outputs: [ 285 | { 286 | address: 287 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 288 | amount: '4820000', 289 | assets: [], 290 | setMax: false, 291 | }, 292 | ], 293 | changeAddress: changeAddress, 294 | certificates: [], 295 | withdrawals: [], 296 | accountPubKey: 297 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 298 | ttl: 123456789, 299 | options: {}, 300 | result: { 301 | totalSpent: '5000000', 302 | fee: '180000', 303 | inputs: [utxo1], 304 | outputs: [ 305 | { 306 | address: 307 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 308 | amount: '4820000', 309 | assets: [], 310 | setMax: false, 311 | }, 312 | ], 313 | ttl: 123456789, 314 | tx: { 315 | body: 'a400d90102818258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d000181825839013af9d8434bea8de03cd698d5fa1c6b82b991146a755f509e95d6b53b15ab05b40d24d39c9d14dfec04d87ed071f2c66484b3ab83ab3d603d1a00498c20021a0002bf20031a075bcd15', 316 | hash: '055d468ab5f751e4a807372a92b2c82b7d6ed4508e3cc49f5550f1e28a904751', 317 | size: 231, 318 | }, 319 | }, 320 | }, 321 | { 322 | description: 'Prefer utxo with largest asset (token) value', 323 | utxos: [utxo2, utxo6], 324 | outputs: [ 325 | { 326 | address: 327 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 328 | amount: undefined, 329 | assets: [ 330 | { 331 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 332 | quantity: '50', 333 | }, 334 | ], 335 | setMax: false, 336 | }, 337 | ], 338 | changeAddress: changeAddress, 339 | certificates: [], 340 | withdrawals: [], 341 | accountPubKey: 342 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 343 | ttl: undefined, 344 | options: {}, 345 | result: { 346 | totalSpent: '1315527', 347 | tx: { 348 | body: 'a300d90102818258209e63fddf20cb7b5472e2c9a1bb4bbe3112b8f2b22e45bc441206bcddde5c58a0070182825839013af9d8434bea8de03cd698d5fa1c6b82b991146a755f509e95d6b53b15ab05b40d24d39c9d14dfec04d87ed071f2c66484b3ab83ab3d603d821a00116d86a1581c02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7aa14447524943183282583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f821a0028f639a2581c02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7aa1444752494319079e581cc6207cbbc916fa3bbb4b91cc7789c7d7ddfb84264fa76f7ee627a9d8a1401864021a0002a541', 349 | hash: 'fe7f5393044fbf142d10cc74ff7c630a76ca0d554c453f91ac9e933cb6be622d', 350 | size: 405, 351 | }, 352 | fee: '173377', 353 | ttl: undefined, 354 | inputs: [utxo6], 355 | outputs: [ 356 | { 357 | address: 358 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 359 | amount: '1142150', 360 | assets: [ 361 | { 362 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 363 | quantity: '50', 364 | }, 365 | ], 366 | setMax: false, 367 | }, 368 | { 369 | isChange: true, 370 | address: changeAddress, 371 | amount: '2684473', 372 | assets: [ 373 | { 374 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 375 | quantity: '1950', 376 | }, 377 | { 378 | quantity: '100', 379 | unit: 'c6207cbbc916fa3bbb4b91cc7789c7d7ddfb84264fa76f7ee627a9d8', 380 | }, 381 | ], 382 | }, 383 | ], 384 | }, 385 | }, 386 | { 387 | description: '_maxTokensPerOutput=1 creates 2 changes', 388 | utxos: [utxo2, utxo8], 389 | outputs: [ 390 | { 391 | address: 392 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 393 | amount: undefined, 394 | assets: [ 395 | { 396 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 397 | quantity: '50', 398 | }, 399 | ], 400 | setMax: false, 401 | }, 402 | ], 403 | changeAddress: changeAddress, 404 | certificates: [], 405 | withdrawals: [], 406 | accountPubKey: 407 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 408 | options: { _maxTokensPerOutput: 1 }, 409 | result: { 410 | tx: { 411 | body: 'a300d90102828258209e63fddf20cb7b5472e2c9a1bb4bbe3112b8f2b22e45bc441206bcddde5c58a0018258209e63fddf20cb7b5472e2c9a1bb4bbe3112b8f2b22e45bc441206bcddde5c58a0070183825839013af9d8434bea8de03cd698d5fa1c6b82b991146a755f509e95d6b53b15ab05b40d24d39c9d14dfec04d87ed071f2c66484b3ab83ab3d603d821a00116d86a1581c02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7aa14447524943183282583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f821a00117e5ca1581c02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7aa14447524943190b8682583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f821a00452ce9a1581cc6207cbbc916fa3bbb4b91cc7789c7d7ddfb84264fa76f7ee627a9d8a1401864021a0002b6f5', 412 | hash: 'f450b09570c608269a680d244b3c52821b6da3b6c45557a5cc718c403856b0dc', 413 | size: 508, 414 | }, 415 | totalSpent: '1320059', 416 | fee: '177909', 417 | inputs: [utxo2, utxo8], 418 | outputs: [ 419 | { 420 | address: 421 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 422 | amount: '1142150', 423 | assets: [ 424 | { 425 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 426 | quantity: '50', 427 | }, 428 | ], 429 | setMax: false, 430 | }, 431 | { 432 | isChange: true, 433 | address: changeAddress, 434 | amount: '1146460', 435 | assets: [ 436 | { 437 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 438 | quantity: '2950', 439 | }, 440 | ], 441 | }, 442 | { 443 | isChange: true, 444 | address: changeAddress, 445 | amount: '4533481', 446 | assets: [ 447 | { 448 | quantity: '100', 449 | unit: 'c6207cbbc916fa3bbb4b91cc7789c7d7ddfb84264fa76f7ee627a9d8', 450 | }, 451 | ], 452 | }, 453 | ], 454 | }, 455 | }, 456 | { 457 | description: 458 | '2 ADA utxos (2 ADA, 1 ADA), needs both in order to return change and not to burn it as unnecessarily high fee', 459 | utxos: [utxo4, utxo5], 460 | outputs: [ 461 | { 462 | address: 463 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 464 | amount: '1000000', 465 | assets: [], 466 | setMax: false, 467 | }, 468 | ], 469 | changeAddress: changeAddress, 470 | certificates: [], 471 | withdrawals: [], 472 | accountPubKey: 473 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 474 | options: {}, 475 | result: { 476 | tx: { 477 | body: 'a300d90102828258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d038258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d040182825839013af9d8434bea8de03cd698d5fa1c6b82b991146a755f509e95d6b53b15ab05b40d24d39c9d14dfec04d87ed071f2c66484b3ab83ab3d603d1a000f424082583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f1a001becd3021a000297ad', 478 | hash: '62f300ab828a6414c37ca91abdf661b1ffe88bbfd61603acf1b8874164286e8b', 479 | size: 326, 480 | }, 481 | totalSpent: '1169901', 482 | fee: '169901', 483 | inputs: [utxo4, utxo5], 484 | outputs: [ 485 | { 486 | address: 487 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 488 | amount: '1000000', 489 | assets: [], 490 | setMax: false, 491 | }, 492 | { 493 | isChange: true, 494 | address: changeAddress, 495 | amount: '1830099', 496 | assets: [], 497 | }, 498 | ], 499 | }, 500 | }, 501 | { 502 | description: '1 ADA only utxo, 1 output + change (custom fee param A=0)', 503 | utxos: [utxo1], 504 | outputs: [ 505 | { 506 | address: 507 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 508 | amount: '3000000', 509 | assets: [], 510 | setMax: false, 511 | }, 512 | ], 513 | changeAddress: changeAddress, 514 | certificates: [], 515 | withdrawals: [], 516 | accountPubKey: 517 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 518 | options: { feeParams: { a: '0' } }, 519 | result: { 520 | tx: { 521 | body: 'a300d90102818258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d000182825839013af9d8434bea8de03cd698d5fa1c6b82b991146a755f509e95d6b53b15ab05b40d24d39c9d14dfec04d87ed071f2c66484b3ab83ab3d603d1a002dc6c082583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f1a001c258b021a00025ef5', 522 | hash: '66a743f7275683799070935d33932319f82467be2eb4bea5b33ab36aaa15c8d6', 523 | size: 290, 524 | }, 525 | totalSpent: '3155381', 526 | fee: '155381', // since we set cost per byte to 0, the tx cost wll be equal to fee param B 527 | inputs: [utxo1], 528 | outputs: [ 529 | { 530 | address: 531 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 532 | amount: '3000000', 533 | assets: [], 534 | setMax: false, 535 | }, 536 | { 537 | isChange: true, 538 | address: changeAddress, 539 | amount: '1844619', 540 | assets: [], 541 | }, 542 | ], 543 | }, 544 | }, 545 | { 546 | description: 'set max on ADA output, no change', 547 | utxos: [utxo1, utxo3], 548 | outputs: [ 549 | { 550 | address: 551 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 552 | amount: undefined, 553 | assets: [], 554 | setMax: true, 555 | }, 556 | ], 557 | changeAddress: changeAddress, 558 | certificates: [], 559 | withdrawals: [], 560 | accountPubKey: 561 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 562 | options: {}, 563 | result: { 564 | tx: { 565 | body: 'a300d90102828258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d008258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d020181825839013af9d8434bea8de03cd698d5fa1c6b82b991146a755f509e95d6b53b15ab05b40d24d39c9d14dfec04d87ed071f2c66484b3ab83ab3d603d1a00e2553f021a00028c81', 566 | hash: 'c00afada3a077ccf79bc4cf3b9b3613f6b8cda19409fe984be696e38188c7cfa', 567 | size: 261, 568 | }, 569 | max: '14832959', 570 | totalSpent: '15000000', 571 | fee: '167041', 572 | inputs: [utxo1, utxo3], 573 | outputs: [ 574 | { 575 | address: 576 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 577 | amount: '14832959', 578 | assets: [], 579 | setMax: true, 580 | }, 581 | ], 582 | }, 583 | }, 584 | { 585 | description: 'set max on ADA output, assets returned', 586 | utxos: [utxo1, utxo2], 587 | outputs: [ 588 | { 589 | address: 590 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 591 | amount: undefined, 592 | assets: [], 593 | setMax: true, 594 | }, 595 | ], 596 | changeAddress: changeAddress, 597 | certificates: [], 598 | withdrawals: [], 599 | accountPubKey: 600 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 601 | options: {}, 602 | result: { 603 | tx: { 604 | body: 'a300d90102828258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d008258209e63fddf20cb7b5472e2c9a1bb4bbe3112b8f2b22e45bc441206bcddde5c58a0010182825839013af9d8434bea8de03cd698d5fa1c6b82b991146a755f509e95d6b53b15ab05b40d24d39c9d14dfec04d87ed071f2c66484b3ab83ab3d603d1a0084796b82583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f821a00117e5ca1581c02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7aa144475249431903e8021a00029eb9', 605 | hash: '9f458c1293fec11c82addef7319e69816d90aeb5dd8aadc051109f929f498497', 606 | size: 367, 607 | }, 608 | max: '8681835', 609 | totalSpent: '8853540', // plus 1344798 in change output = 10000000 610 | fee: '171705', 611 | inputs: [utxo1, utxo2], 612 | outputs: [ 613 | { 614 | address: 615 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 616 | amount: '8681835', 617 | assets: [], 618 | setMax: true, 619 | }, 620 | { 621 | isChange: true, 622 | address: changeAddress, 623 | amount: '1146460', 624 | assets: [ 625 | { 626 | quantity: '1000', 627 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 628 | }, 629 | ], 630 | }, 631 | ], 632 | }, 633 | }, 634 | { 635 | description: 'set max on ADA output, multiple outputs, assets returned', 636 | utxos: [utxo1, utxo2], 637 | outputs: [ 638 | { 639 | address: 640 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 641 | amount: '1000000', 642 | assets: [], 643 | setMax: false, 644 | }, 645 | { 646 | address: 647 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 648 | amount: undefined, 649 | assets: [], 650 | setMax: true, 651 | }, 652 | ], 653 | changeAddress: changeAddress, 654 | certificates: [], 655 | withdrawals: [], 656 | accountPubKey: 657 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 658 | options: {}, 659 | result: { 660 | tx: { 661 | body: 'a300d90102828258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d008258209e63fddf20cb7b5472e2c9a1bb4bbe3112b8f2b22e45bc441206bcddde5c58a0010183825839013af9d8434bea8de03cd698d5fa1c6b82b991146a755f509e95d6b53b15ab05b40d24d39c9d14dfec04d87ed071f2c66484b3ab83ab3d603d1a000f4240825839013af9d8434bea8de03cd698d5fa1c6b82b991146a755f509e95d6b53b15ab05b40d24d39c9d14dfec04d87ed071f2c66484b3ab83ab3d603d1a00752bff82583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f821a00117e5ca1581c02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7aa144475249431903e8021a0002a9e5', 662 | hash: 'c5a511df10142ce8c189164143bde1472f1df48a82f4818dd9f82786c2ce841f', 663 | size: 432, 664 | }, 665 | max: '7678975', 666 | totalSpent: '8853540', // plus 1146460 in change output = 10000000 667 | fee: '174565', 668 | inputs: [utxo1, utxo2], 669 | outputs: [ 670 | { 671 | address: 672 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 673 | amount: '1000000', 674 | assets: [], 675 | setMax: false, 676 | }, 677 | { 678 | address: 679 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 680 | amount: '7678975', 681 | assets: [], 682 | setMax: true, 683 | }, 684 | { 685 | isChange: true, 686 | address: changeAddress, 687 | amount: '1146460', 688 | assets: [ 689 | { 690 | quantity: '1000', 691 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 692 | }, 693 | ], 694 | }, 695 | ], 696 | }, 697 | }, 698 | { 699 | description: 'set max on token output', 700 | utxos: [utxo1, utxo7], 701 | outputs: [ 702 | { 703 | address: 704 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 705 | amount: undefined, 706 | assets: [ 707 | { 708 | quantity: '', 709 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 710 | }, 711 | ], 712 | setMax: true, 713 | }, 714 | ], 715 | changeAddress: changeAddress, 716 | certificates: [], 717 | withdrawals: [], 718 | accountPubKey: 719 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 720 | options: {}, 721 | result: { 722 | tx: { 723 | body: 'a300d90102828258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d008258209e63fddf20cb7b5472e2c9a1bb4bbe3112b8f2b22e45bc441206bcddde5c58a0080182825839013af9d8434bea8de03cd698d5fa1c6b82b991146a755f509e95d6b53b15ab05b40d24d39c9d14dfec04d87ed071f2c66484b3ab83ab3d603d821a00117e5ca1581c02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7aa144475249431903e882583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f1a004db1fb021a00029eb9', 724 | hash: '9f1a8906f54674180707eef5d42bfcf9e0baea67b6ce730e974022175bb823bb', 725 | size: 367, 726 | }, 727 | max: '1000', 728 | totalSpent: '1318165', // plus amount in change output = 6410000 729 | fee: '171705', 730 | inputs: [utxo1, utxo7], 731 | outputs: [ 732 | { 733 | address: 734 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 735 | amount: '1146460', 736 | assets: [ 737 | { 738 | quantity: '1000', 739 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 740 | }, 741 | ], 742 | setMax: true, 743 | }, 744 | { 745 | isChange: true, 746 | address: changeAddress, 747 | amount: '5091835', 748 | assets: [], 749 | }, 750 | ], 751 | }, 752 | }, 753 | { 754 | description: 'withdrawing rewards: 1 ADA only utxo, 1 change output', 755 | utxos: [utxo1], 756 | outputs: [], 757 | changeAddress: changeAddress, 758 | certificates: [], 759 | withdrawals: [ 760 | { 761 | amount: '10000000', 762 | stakingPath: "m/1852'/1815'/0'/2/0", 763 | stakeAddress: 764 | 'stake1u8yk3dcuj8yylwvnzz953yups6mmuvt0vtjmxl2gmgceqjqz2yfd2', 765 | }, 766 | ], 767 | accountPubKey: 768 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 769 | options: {}, 770 | result: { 771 | tx: { 772 | body: 'a400d90102818258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d00018182583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f1a00e2438b021a00029e3505a1581de1c968b71c91c84fb993108b48938186b7be316f62e5b37d48da3190481a00989680', 773 | hash: '1b252c5dad4cdf492bec64c76936d0ac8e8c9c4b4a06c3fd908c2c3bf2797aef', 774 | size: 364, 775 | }, 776 | totalSpent: '171573', 777 | fee: '171573', 778 | inputs: [utxo1], 779 | outputs: [ 780 | { 781 | isChange: true, 782 | address: changeAddress, 783 | amount: '14828427', 784 | assets: [], 785 | }, 786 | ], 787 | }, 788 | }, 789 | { 790 | description: 791 | 'withdrawing rewards: 1 ADA only utxo, 1 change output, vote drep keyhash', 792 | utxos: [utxo1], 793 | outputs: [], 794 | changeAddress: changeAddress, 795 | certificates: [ 796 | { 797 | type: 9, 798 | dRep: { 799 | type: 0, // keyhash 800 | keyHash: '4519f294d80b0fcc6697bde8f36629be8ebf9527be023fe73673f1a9', 801 | }, 802 | } as CertificateVoteDelegation, 803 | ], 804 | withdrawals: [ 805 | { 806 | amount: '10000000', 807 | stakingPath: "m/1852'/1815'/0'/2/0", 808 | stakeAddress: 809 | 'stake1u8yk3dcuj8yylwvnzz953yups6mmuvt0vtjmxl2gmgceqjqz2yfd2', 810 | }, 811 | ], 812 | accountPubKey: 813 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 814 | options: {}, 815 | result: { 816 | tx: { 817 | body: 'a500d90102818258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d00018182583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f1a00e225fb021a0002bbc504d901028183098200581cfb52d3055a3a3a3238ce219a3fc13fe4d8797d5062e8dd4670c7d29b8200581c4519f294d80b0fcc6697bde8f36629be8ebf9527be023fe73673f1a905a1581de1c968b71c91c84fb993108b48938186b7be316f62e5b37d48da3190481a00989680', 818 | hash: '8a3a95b836262421593b301c131edf9075adf4ec9d9aa4ef7b738f77da892ad3', 819 | size: 536, 820 | }, 821 | totalSpent: '179141', 822 | fee: '179141', 823 | deposit: '0', 824 | inputs: [utxo1], 825 | outputs: [ 826 | { 827 | isChange: true, 828 | address: changeAddress, 829 | amount: '14820859', 830 | assets: [], 831 | }, 832 | ], 833 | }, 834 | }, 835 | { 836 | description: 837 | 'withdrawing rewards: 1 ADA only utxo, 1 change output, vote delegation abstain', 838 | utxos: [utxo1], 839 | outputs: [], 840 | changeAddress: changeAddress, 841 | certificates: [ 842 | { 843 | type: 9, 844 | dRep: { 845 | type: 2, // abstain 846 | }, 847 | } as CertificateVoteDelegation, 848 | ], 849 | withdrawals: [ 850 | { 851 | amount: '10000000', 852 | stakingPath: "m/1852'/1815'/0'/2/0", 853 | stakeAddress: 854 | 'stake1u8yk3dcuj8yylwvnzz953yups6mmuvt0vtjmxl2gmgceqjqz2yfd2', 855 | }, 856 | ], 857 | accountPubKey: 858 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 859 | options: {}, 860 | result: { 861 | tx: { 862 | body: 'a500d90102818258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d00018182583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f1a00e22b23021a0002b69d04d901028183098200581cfb52d3055a3a3a3238ce219a3fc13fe4d8797d5062e8dd4670c7d29b810205a1581de1c968b71c91c84fb993108b48938186b7be316f62e5b37d48da3190481a00989680', 863 | hash: '03e9fc7cf58569d36d5e3bd61a5d588121e2a13818ad29920b17dfe186a9bc5b', 864 | size: 506, 865 | }, 866 | totalSpent: '177821', 867 | fee: '177821', 868 | deposit: '0', 869 | inputs: [utxo1], 870 | outputs: [ 871 | { 872 | isChange: true, 873 | address: changeAddress, 874 | amount: '14822179', 875 | assets: [], 876 | }, 877 | ], 878 | }, 879 | }, 880 | { 881 | description: 882 | 'withdrawing rewards: 1 ADA only utxo, 1 change output, vote delegation drep id', 883 | utxos: [utxo1], 884 | outputs: [], 885 | changeAddress: changeAddress, 886 | certificates: [ 887 | { 888 | type: 9, 889 | dRep: { 890 | type: CardanoDRepType.KEY_HASH, 891 | keyHash: '3a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c490711', 892 | }, 893 | } as CertificateVoteDelegation, 894 | ], 895 | withdrawals: [ 896 | { 897 | amount: '10000000', 898 | stakingPath: "m/1852'/1815'/0'/2/0", 899 | stakeAddress: 900 | 'stake1u8yk3dcuj8yylwvnzz953yups6mmuvt0vtjmxl2gmgceqjqz2yfd2', 901 | }, 902 | ], 903 | accountPubKey: 904 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 905 | options: {}, 906 | result: { 907 | tx: { 908 | body: 'a500d90102818258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d00018182583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f1a00e225fb021a0002bbc504d901028183098200581cfb52d3055a3a3a3238ce219a3fc13fe4d8797d5062e8dd4670c7d29b8200581c3a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c49071105a1581de1c968b71c91c84fb993108b48938186b7be316f62e5b37d48da3190481a00989680', 909 | hash: '761e3e27851f05504dc58a792a47681fe97e6defaea637fddd485c0b468789b9', 910 | size: 536, 911 | }, 912 | totalSpent: '179141', 913 | fee: '179141', 914 | deposit: '0', 915 | inputs: [utxo1], 916 | outputs: [ 917 | { 918 | isChange: true, 919 | address: changeAddress, 920 | amount: '14820859', 921 | assets: [], 922 | }, 923 | ], 924 | }, 925 | }, 926 | { 927 | description: 928 | 'withdrawing rewards: multiple utxos, multiple withdrawals, 1 change output', 929 | utxos: [utxo1, utxo2], 930 | outputs: [], 931 | changeAddress: changeAddress, 932 | certificates: [], 933 | withdrawals: [ 934 | { 935 | amount: '10000000', 936 | stakingPath: "m/1852'/1815'/0'/2/0", 937 | stakeAddress: 938 | 'stake1u8yk3dcuj8yylwvnzz953yups6mmuvt0vtjmxl2gmgceqjqz2yfd2', 939 | }, 940 | { 941 | amount: '10000000', 942 | stakingPath: "m/1852'/1815'/1'/2/0", 943 | stakeAddress: 944 | 'stake1u8yk3dcuj8yylwvnzz953yups6mmuvt0vtjmxl2gmgceqjqz2yfd2', 945 | }, 946 | ], 947 | accountPubKey: 948 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 949 | options: {}, 950 | result: { 951 | tx: { 952 | body: 'a400d90102818258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d00018182583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f1a017ada0b021a00029e3505a1581de1c968b71c91c84fb993108b48938186b7be316f62e5b37d48da3190481a00989680', 953 | hash: '796c2a23b17593c090d2b6b343cdf14dedd1eb417c9a5b583567e44b52ed2888', 954 | size: 364, 955 | }, 956 | totalSpent: '171573', 957 | fee: '171573', 958 | inputs: [utxo1], 959 | outputs: [ 960 | { 961 | isChange: true, 962 | address: changeAddress, 963 | amount: '24828427', 964 | assets: [], 965 | }, 966 | ], 967 | }, 968 | }, 969 | { 970 | description: 'stake registration', 971 | utxos: [utxo1], 972 | outputs: [], 973 | changeAddress: changeAddress, 974 | certificates: [ 975 | { 976 | type: 0, 977 | }, 978 | ] as Certificate[], 979 | withdrawals: [], 980 | accountPubKey: 981 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 982 | options: {}, 983 | result: { 984 | tx: { 985 | body: 'a400d90102818258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d00018182583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f1a002b39bb021a00028d0504d901028182008200581cfb52d3055a3a3a3238ce219a3fc13fe4d8797d5062e8dd4670c7d29b', 986 | hash: '981b99d39e65e2697ab277c8dabf39a779a862480e2e214d5d55251ba6d94f52', 987 | size: 264, 988 | }, 989 | totalSpent: '2167173', 990 | fee: '167173', 991 | inputs: [utxo1], 992 | outputs: [ 993 | { 994 | isChange: true, 995 | address: changeAddress, 996 | amount: '2832827', 997 | assets: [], 998 | }, 999 | ], 1000 | }, 1001 | }, 1002 | { 1003 | description: 'stake delegation', 1004 | utxos: [utxo1], 1005 | outputs: [], 1006 | changeAddress: changeAddress, 1007 | certificates: [ 1008 | { 1009 | type: 2 as const, 1010 | 1011 | pool: '0f292fcaa02b8b2f9b3c8f9fd8e0bb21abedb692a6d5058df3ef2735', 1012 | }, 1013 | ] as Certificate[], 1014 | withdrawals: [], 1015 | accountPubKey: 1016 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 1017 | options: {}, 1018 | result: { 1019 | tx: { 1020 | body: 'a400d90102818258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d00018182583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f1a0049a7b7021a0002a38904d901028183028200581cfb52d3055a3a3a3238ce219a3fc13fe4d8797d5062e8dd4670c7d29b581c0f292fcaa02b8b2f9b3c8f9fd8e0bb21abedb692a6d5058df3ef2735', 1021 | hash: '37717824e17580f9a47b4753cedd8ae74d0ada1290087a4cca038faa2979a70e', 1022 | size: 395, 1023 | }, 1024 | totalSpent: '172937', 1025 | fee: '172937', 1026 | inputs: [utxo1], 1027 | outputs: [ 1028 | { 1029 | isChange: true, 1030 | address: changeAddress, 1031 | amount: '4827063', 1032 | assets: [], 1033 | }, 1034 | ], 1035 | }, 1036 | }, 1037 | { 1038 | description: 'stake deregistration', 1039 | utxos: [utxo1], 1040 | outputs: [], 1041 | changeAddress: changeAddress, 1042 | certificates: [ 1043 | { 1044 | type: 1, 1045 | }, 1046 | ] as Certificate[], 1047 | withdrawals: [], 1048 | accountPubKey: 1049 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 1050 | options: {}, 1051 | result: { 1052 | tx: { 1053 | body: 'a400d90102818258203c388acb799a37a4f1cc99bec7626637b0b80626b9ef7c7a687282cab701178d00018182583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f1a0068315f021a00029e6104d901028182018200581cfb52d3055a3a3a3238ce219a3fc13fe4d8797d5062e8dd4670c7d29b', 1054 | hash: 'bdfac46c16d64db6e5952cc9a579660394ff8cb2386678dd635fbf8243fa89c4', 1055 | size: 365, 1056 | }, 1057 | totalSpent: '171617', 1058 | fee: '171617', 1059 | inputs: [utxo1], 1060 | outputs: [ 1061 | { 1062 | isChange: true, 1063 | address: changeAddress, 1064 | amount: '6828383', 1065 | assets: [], 1066 | }, 1067 | ], 1068 | }, 1069 | }, 1070 | { 1071 | description: 1072 | 'multiple utxos (including multi asset), 2 user defined outputs (ADA only) + 1 change output (with an asset)', 1073 | utxos: [ 1074 | { 1075 | address: 1076 | 'addr1q9vf2uqwv9cx23rsfeqqa4g9rv8s2ha464sycdwpzdhm7ana9nxu0t6xjurg0qqcwwdulh56uglsp8z2uw9wuzjtfuaqka2l2d', 1077 | txHash: 1078 | '1bfb8b1d06bd28fb33493afaa5b22dec02bb8e292bbd7a6965c9037b5964a808', 1079 | outputIndex: 1, 1080 | amount: [ 1081 | { 1082 | quantity: '3831173306', 1083 | unit: 'lovelace', 1084 | }, 1085 | { 1086 | quantity: '998900', 1087 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 1088 | }, 1089 | ], 1090 | }, 1091 | { 1092 | address: 1093 | 'addr1qydd8kf2vtzv05y703kvsq0tcrgnwynqemxkp7rw4nwnq2ma9nxu0t6xjurg0qqcwwdulh56uglsp8z2uw9wuzjtfuaqqdz40d', 1094 | txHash: 1095 | '064ebc7096680b94de4e1c014938ea44886829c08ec01025578104b7b60d6bcf', 1096 | outputIndex: 1, 1097 | amount: [ 1098 | { 1099 | quantity: '848035104', 1100 | unit: 'lovelace', 1101 | }, 1102 | ], 1103 | }, 1104 | { 1105 | address: 1106 | 'addr1q80vvrk4syazwl6w706ah9rgzvr5heq6hq2tjqxxu3x8wnma9nxu0t6xjurg0qqcwwdulh56uglsp8z2uw9wuzjtfuaqvmacgp', 1107 | txHash: 1108 | 'd49c08164d3f2abc1a2e1b16a2c81122240a99bb6bd2f8d33628048df7529adc', 1109 | outputIndex: 1, 1110 | amount: [ 1111 | { 1112 | quantity: '1180285694', 1113 | unit: 'lovelace', 1114 | }, 1115 | ], 1116 | }, 1117 | { 1118 | address: 1119 | 'addr1q984228shp7a6m0xrj39k7uuvcpsqt2dkjn8r6lvrpnmdfna9nxu0t6xjurg0qqcwwdulh56uglsp8z2uw9wuzjtfuaq77p62n', 1120 | txHash: 1121 | 'ca0bad48270c5345bbcce7a850f545be5582a780d5a0337385d1b7413dfc60e3', 1122 | outputIndex: 0, 1123 | amount: [ 1124 | { 1125 | quantity: '2000000', 1126 | unit: 'lovelace', 1127 | }, 1128 | ], 1129 | }, 1130 | ], 1131 | outputs: [ 1132 | { 1133 | address: 1134 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 1135 | amount: '1000000', 1136 | assets: [], 1137 | setMax: false, 1138 | }, 1139 | { 1140 | address: 1141 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 1142 | amount: '2000000', 1143 | assets: [], 1144 | setMax: false, 1145 | }, 1146 | ], 1147 | changeAddress: changeAddress, 1148 | certificates: [], 1149 | withdrawals: [], 1150 | accountPubKey: 1151 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 1152 | options: {}, 1153 | result: { 1154 | tx: { 1155 | body: 'a300d90102818258201bfb8b1d06bd28fb33493afaa5b22dec02bb8e292bbd7a6965c9037b5964a808010183825839013af9d8434bea8de03cd698d5fa1c6b82b991146a755f509e95d6b53b15ab05b40d24d39c9d14dfec04d87ed071f2c66484b3ab83ab3d603d1a000f4240825839013af9d8434bea8de03cd698d5fa1c6b82b991146a755f509e95d6b53b15ab05b40d24d39c9d14dfec04d87ed071f2c66484b3ab83ab3d603d1a001e848082583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f821ae42aa5eda1581c02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7aa144475249431a000f3df4021a0002a40d', 1156 | hash: '458c3316af1e157a1d5576f381bbff2dc26af3c6ea9c87b951b08777255699d3', 1157 | size: 398, 1158 | }, 1159 | totalSpent: '3173069', 1160 | fee: '173069', 1161 | inputs: [ 1162 | { 1163 | address: 1164 | 'addr1q9vf2uqwv9cx23rsfeqqa4g9rv8s2ha464sycdwpzdhm7ana9nxu0t6xjurg0qqcwwdulh56uglsp8z2uw9wuzjtfuaqka2l2d', 1165 | txHash: 1166 | '1bfb8b1d06bd28fb33493afaa5b22dec02bb8e292bbd7a6965c9037b5964a808', 1167 | outputIndex: 1, 1168 | amount: [ 1169 | { 1170 | quantity: '3831173306', 1171 | unit: 'lovelace', 1172 | }, 1173 | { 1174 | quantity: '998900', 1175 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 1176 | }, 1177 | ], 1178 | }, 1179 | ], 1180 | outputs: [ 1181 | { 1182 | address: 1183 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 1184 | amount: '1000000', 1185 | assets: [], 1186 | setMax: false, 1187 | }, 1188 | { 1189 | address: 1190 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 1191 | amount: '2000000', 1192 | assets: [], 1193 | setMax: false, 1194 | }, 1195 | { 1196 | amount: '3828000237', 1197 | isChange: true, 1198 | address: changeAddress, 1199 | assets: [ 1200 | { 1201 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 1202 | quantity: '998900', 1203 | }, 1204 | ], 1205 | }, 1206 | ], 1207 | }, 1208 | }, 1209 | { 1210 | description: 'Correctly recalculate change output after set max on token', 1211 | utxos: [ 1212 | { 1213 | address: 1214 | 'addr_test1qzhts48qcr2s76qcrh4rvwwah5xc52g7fr4xtzfzq9ffxme9j9xney949w6u957hfn5r7gmlh789208l4g9cal4m9p3qyt8vq9', 1215 | txHash: 1216 | 'd8ff7a39d1daf80ae2e99351c51fbb823f223e717cee09d23bc1b2691092632d', 1217 | outputIndex: 0, 1218 | amount: [ 1219 | { 1220 | quantity: '1344798', 1221 | unit: 'lovelace', 1222 | }, 1223 | { 1224 | quantity: '1', 1225 | unit: '3b746b6a5f8c43acc6bed9259ff7fc5f0b9e0be8adc3d63edfea98c77072657373757265', 1226 | }, 1227 | ], 1228 | }, 1229 | { 1230 | address: 1231 | 'addr_test1qq6r7mgs3q42n8ja7sf9wkn747lnttke2zx7khdrc5a2e4p9j9xney949w6u957hfn5r7gmlh789208l4g9cal4m9p3q83lu76', 1232 | txHash: 1233 | '280c49a69c0fc24c3fdcdbcdd4030da3533a87e4378639bcd4a8841b3d2c6e21', 1234 | outputIndex: 2, 1235 | amount: [ 1236 | { 1237 | quantity: '1344798', 1238 | unit: 'lovelace', 1239 | }, 1240 | ], 1241 | }, 1242 | { 1243 | address: 1244 | 'addr_test1qzhts48qcr2s76qcrh4rvwwah5xc52g7fr4xtzfzq9ffxme9j9xney949w6u957hfn5r7gmlh789208l4g9cal4m9p3qyt8vq9', 1245 | txHash: 1246 | '05cf0d8c9824b6e1bf403329d159cc57d89b4c22d93835bbdee46687f7c69c69', 1247 | outputIndex: 0, 1248 | amount: [ 1249 | { 1250 | quantity: '1344798', 1251 | unit: 'lovelace', 1252 | }, 1253 | { 1254 | quantity: '1', 1255 | unit: '4f740e06506c0b8a1584760780ce3c61aea3b6061d5596d580e9aae66265726e617264', 1256 | }, 1257 | ], 1258 | }, 1259 | ], 1260 | outputs: [ 1261 | { 1262 | address: 1263 | 'addr_test1qztq45fff6e84v0qctnzpg86lny8zf7nx0lmn47p3msk5tvw8qpyux7tm435rjuk5dqr6kny4ks9w7vqjnfrsvtjk82quqe75s', 1264 | assets: [ 1265 | { 1266 | unit: '3b746b6a5f8c43acc6bed9259ff7fc5f0b9e0be8adc3d63edfea98c77072657373757265', 1267 | quantity: '1', 1268 | }, 1269 | ], 1270 | setMax: false, 1271 | }, 1272 | { 1273 | address: 1274 | 'addr_test1qztq45fff6e84v0qctnzpg86lny8zf7nx0lmn47p3msk5tvw8qpyux7tm435rjuk5dqr6kny4ks9w7vqjnfrsvtjk82quqe75s', 1275 | assets: [ 1276 | { 1277 | unit: '4f740e06506c0b8a1584760780ce3c61aea3b6061d5596d580e9aae66265726e617264', 1278 | quantity: '', 1279 | }, 1280 | ], 1281 | setMax: true, 1282 | }, 1283 | ], 1284 | changeAddress: changeAddress, 1285 | certificates: [], 1286 | withdrawals: [], 1287 | accountPubKey: 1288 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 1289 | options: {}, 1290 | result: { 1291 | tx: { 1292 | body: 'a300d901028382582005cf0d8c9824b6e1bf403329d159cc57d89b4c22d93835bbdee46687f7c69c6900825820280c49a69c0fc24c3fdcdbcdd4030da3533a87e4378639bcd4a8841b3d2c6e2102825820d8ff7a39d1daf80ae2e99351c51fbb823f223e717cee09d23bc1b2691092632d00018382583900960ad1294eb27ab1e0c2e620a0fafcc87127d333ffb9d7c18ee16a2d8e38024e1bcbdd6341cb96a3403d5a64ada057798094d2383172b1d4821a0011a008a1581c3b746b6a5f8c43acc6bed9259ff7fc5f0b9e0be8adc3d63edfea98c7a14870726573737572650182583900960ad1294eb27ab1e0c2e620a0fafcc87127d333ffb9d7c18ee16a2d8e38024e1bcbdd6341cb96a3403d5a64ada057798094d2383172b1d4821a00118f32a1581c4f740e06506c0b8a1584760780ce3c61aea3b6061d5596d580e9aae6a1476265726e6172640182583901f8a4be8308c12b910252b6fd6ee4a98730300009382becc049a6e618476aacdafaf01e68c2f072270f078c9689da6139eba4b309e1d5615f1a0017971f021a0002c901', 1293 | hash: '439134fd244df14ce5c2b0e17f5b6a73b51f850ed617e9b15439dbe917e37a97', 1294 | size: 613, 1295 | }, 1296 | totalSpent: '2488379', 1297 | fee: '182529', 1298 | inputs: [ 1299 | { 1300 | address: 1301 | 'addr_test1qzhts48qcr2s76qcrh4rvwwah5xc52g7fr4xtzfzq9ffxme9j9xney949w6u957hfn5r7gmlh789208l4g9cal4m9p3qyt8vq9', 1302 | txHash: 1303 | '05cf0d8c9824b6e1bf403329d159cc57d89b4c22d93835bbdee46687f7c69c69', 1304 | outputIndex: 0, 1305 | amount: [ 1306 | { 1307 | quantity: '1344798', 1308 | unit: 'lovelace', 1309 | }, 1310 | { 1311 | quantity: '1', 1312 | unit: '4f740e06506c0b8a1584760780ce3c61aea3b6061d5596d580e9aae66265726e617264', 1313 | }, 1314 | ], 1315 | }, 1316 | { 1317 | address: 1318 | 'addr_test1qq6r7mgs3q42n8ja7sf9wkn747lnttke2zx7khdrc5a2e4p9j9xney949w6u957hfn5r7gmlh789208l4g9cal4m9p3q83lu76', 1319 | txHash: 1320 | '280c49a69c0fc24c3fdcdbcdd4030da3533a87e4378639bcd4a8841b3d2c6e21', 1321 | outputIndex: 2, 1322 | amount: [ 1323 | { 1324 | quantity: '1344798', 1325 | unit: 'lovelace', 1326 | }, 1327 | ], 1328 | }, 1329 | { 1330 | address: 1331 | 'addr_test1qzhts48qcr2s76qcrh4rvwwah5xc52g7fr4xtzfzq9ffxme9j9xney949w6u957hfn5r7gmlh789208l4g9cal4m9p3qyt8vq9', 1332 | txHash: 1333 | 'd8ff7a39d1daf80ae2e99351c51fbb823f223e717cee09d23bc1b2691092632d', 1334 | outputIndex: 0, 1335 | amount: [ 1336 | { 1337 | quantity: '1344798', 1338 | unit: 'lovelace', 1339 | }, 1340 | { 1341 | quantity: '1', 1342 | unit: '3b746b6a5f8c43acc6bed9259ff7fc5f0b9e0be8adc3d63edfea98c77072657373757265', 1343 | }, 1344 | ], 1345 | }, 1346 | ], 1347 | outputs: [ 1348 | { 1349 | address: 1350 | 'addr_test1qztq45fff6e84v0qctnzpg86lny8zf7nx0lmn47p3msk5tvw8qpyux7tm435rjuk5dqr6kny4ks9w7vqjnfrsvtjk82quqe75s', 1351 | amount: '1155080', 1352 | assets: [ 1353 | { 1354 | quantity: '1', 1355 | unit: '3b746b6a5f8c43acc6bed9259ff7fc5f0b9e0be8adc3d63edfea98c77072657373757265', 1356 | }, 1357 | ], 1358 | setMax: false, 1359 | }, 1360 | { 1361 | address: 1362 | 'addr_test1qztq45fff6e84v0qctnzpg86lny8zf7nx0lmn47p3msk5tvw8qpyux7tm435rjuk5dqr6kny4ks9w7vqjnfrsvtjk82quqe75s', 1363 | amount: '1150770', 1364 | assets: [ 1365 | { 1366 | quantity: '1', 1367 | unit: '4f740e06506c0b8a1584760780ce3c61aea3b6061d5596d580e9aae66265726e617264', 1368 | }, 1369 | ], 1370 | setMax: true, 1371 | }, 1372 | { 1373 | address: 1374 | 'addr1q8u2f05rprqjhygz22m06mhy4xrnqvqqpyuzhmxqfxnwvxz8d2kd47hsre5v9urjyu8s0ryk38dxzw0t5jesncw4v90s22tk0f', 1375 | amount: '1546015', 1376 | assets: [], 1377 | isChange: true, 1378 | }, 1379 | ], 1380 | }, 1381 | }, 1382 | ]; 1383 | 1384 | export const exceptions = [ 1385 | { 1386 | description: 'Not enough utxos to cover an output amount', 1387 | utxos: [utxo1], 1388 | outputs: [ 1389 | { 1390 | address: 1391 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 1392 | amount: '10000000', 1393 | assets: [], 1394 | setMax: false, 1395 | }, 1396 | ], 1397 | changeAddress: changeAddress, 1398 | certificates: [], 1399 | withdrawals: [], 1400 | accountPubKey: 1401 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 1402 | options: {}, 1403 | result: 'UTXO_BALANCE_INSUFFICIENT', 1404 | }, 1405 | { 1406 | description: 1407 | 'Not enough utxos to cover mandatory change output (multi asset utxo)', 1408 | utxos: [utxo2], 1409 | outputs: [ 1410 | { 1411 | address: 1412 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 1413 | amount: '4800000', 1414 | assets: [], 1415 | setMax: false, 1416 | }, 1417 | ], 1418 | changeAddress: changeAddress, 1419 | certificates: [], 1420 | withdrawals: [], 1421 | accountPubKey: 1422 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 1423 | options: {}, 1424 | result: 'UTXO_BALANCE_INSUFFICIENT', 1425 | }, 1426 | { 1427 | description: 'Computed max output amount is lower than minUtxoVal', 1428 | // utxos: 3.344443 ADA, outputs: 1 ADA + 1.443 ADA in change output + fee. This leaves less than 1 ADA which would be set as "max" 1429 | utxos: [ 1430 | { 1431 | address: 1432 | 'addr1q860vxljhadqxnrrsr2j6yxnwpdkyquq74lmghx502aj0r28d2kd47hsre5v9urjyu8s0ryk38dxzw0t5jesncw4v90sp0878u', 1433 | txHash: 1434 | 'b5d1abd05c1eb0564a34c5daa4a71185aa11568c375ab7f946da889ebcb23a01', 1435 | outputIndex: 1, 1436 | amount: [ 1437 | { 1438 | quantity: '1900000', 1439 | unit: 'lovelace', 1440 | }, 1441 | { 1442 | quantity: '90', 1443 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 1444 | }, 1445 | ], 1446 | }, 1447 | { 1448 | address: 1449 | 'addr1q860vxljhadqxnrrsr2j6yxnwpdkyquq74lmghx502aj0r28d2kd47hsre5v9urjyu8s0ryk38dxzw0t5jesncw4v90sp0878u', 1450 | txHash: 1451 | 'b5d1abd05c1eb0564a34c5daa4a71185aa11568c375ab7f946da889ebcb23a01', 1452 | outputIndex: 0, 1453 | amount: [ 1454 | { 1455 | quantity: '1344798', 1456 | unit: 'lovelace', 1457 | }, 1458 | { 1459 | quantity: '10', 1460 | unit: '02477d7c23b4c2834b0be8ca8578dde47af0cc82a964688f6fc95a7a47524943', 1461 | }, 1462 | ], 1463 | }, 1464 | ], 1465 | outputs: [ 1466 | { 1467 | address: 1468 | 'addr1q860vxljhadqxnrrsr2j6yxnwpdkyquq74lmghx502aj0r28d2kd47hsre5v9urjyu8s0ryk38dxzw0t5jesncw4v90sp0878u', 1469 | amount: undefined, 1470 | assets: [], 1471 | setMax: true, 1472 | }, 1473 | { 1474 | address: 1475 | 'addr1q860vxljhadqxnrrsr2j6yxnwpdkyquq74lmghx502aj0r28d2kd47hsre5v9urjyu8s0ryk38dxzw0t5jesncw4v90sp0878u', 1476 | amount: '1000000', 1477 | assets: [], 1478 | setMax: false, 1479 | }, 1480 | ], 1481 | changeAddress: changeAddress, 1482 | certificates: [], 1483 | withdrawals: [], 1484 | accountPubKey: 1485 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 1486 | options: {}, 1487 | result: 'UTXO_BALANCE_INSUFFICIENT', 1488 | }, 1489 | ]; 1490 | 1491 | export const params = [ 1492 | { 1493 | description: 'ttl', 1494 | utxos: [utxo1], 1495 | outputs: [ 1496 | { 1497 | address: 1498 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 1499 | amount: undefined, 1500 | assets: [], 1501 | setMax: false, 1502 | }, 1503 | ], 1504 | changeAddress: changeAddress, 1505 | certificates: [], 1506 | withdrawals: [], 1507 | accountPubKey: 1508 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 1509 | options: {}, 1510 | ttl: undefined, 1511 | result: { 1512 | ttl: undefined, 1513 | }, 1514 | }, 1515 | { 1516 | description: 'ttl', 1517 | utxos: [utxo1], 1518 | outputs: [ 1519 | { 1520 | address: 1521 | 'addr1qya0nkzrf04gmcpu66vdt7sudwptnyg5df6475y7jhtt2wc44vzmgrfy6wwf69xlaszdslksw8evveyykw4c82eavq7sx29tlc', 1522 | amount: undefined, 1523 | assets: [], 1524 | setMax: false, 1525 | }, 1526 | ], 1527 | changeAddress: changeAddress, 1528 | certificates: [], 1529 | withdrawals: [], 1530 | accountPubKey: 1531 | 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', 1532 | options: {}, 1533 | ttl: 123456789, 1534 | result: { 1535 | ttl: 123456789, 1536 | }, 1537 | }, 1538 | ]; 1539 | --------------------------------------------------------------------------------