diff --git a/.changeset/red-wasps-smash.md b/.changeset/red-wasps-smash.md new file mode 100644 index 00000000..40dfa9da --- /dev/null +++ b/.changeset/red-wasps-smash.md @@ -0,0 +1,7 @@ +--- +"@mimicprotocol/lib-ts": patch +"@mimicprotocol/cli": patch +"@mimicprotocol/test-ts": patch +--- + +Refactor Result to use unwrap instead of value diff --git a/packages/cli/src/lib/AbisInterfaceGenerator/FunctionHandler.ts b/packages/cli/src/lib/AbisInterfaceGenerator/FunctionHandler.ts index 728c6bc5..2d2ec830 100644 --- a/packages/cli/src/lib/AbisInterfaceGenerator/FunctionHandler.ts +++ b/packages/cli/src/lib/AbisInterfaceGenerator/FunctionHandler.ts @@ -144,7 +144,7 @@ export default class FunctionHandler { if (isVoid) { lines.push(`return Result.ok<${LibTypes.Void}, string>(new ${LibTypes.Void}())`) } else { - lines.push(`const decoded = ${contractName}Utils.decode${capitalizedName}(response.value)`) + lines.push(`const decoded = ${contractName}Utils.decode${capitalizedName}(response.unwrap())`) lines.push(`return Result.ok<${returnType}, string>(decoded)`) } diff --git a/packages/cli/tests/AbisInterfaceGenerator.spec.ts b/packages/cli/tests/AbisInterfaceGenerator.spec.ts index 2439b380..e6d47835 100644 --- a/packages/cli/tests/AbisInterfaceGenerator.spec.ts +++ b/packages/cli/tests/AbisInterfaceGenerator.spec.ts @@ -240,7 +240,7 @@ describe('AbisInterfaceGenerator', () => { `const response = environment.evmCallQuery(this._address, this._chainId, encodedData.toHexString(), this._timestamp)` ) expect(result).to.contain(`if (response.isError) return Result.err<${LibTypes.BigInt}, string>(response.error)`) - expect(result).to.contain(`const decoded = ${CONTRACT_NAME}Utils.decodeGetBalance(response.value)`) + expect(result).to.contain(`const decoded = ${CONTRACT_NAME}Utils.decodeGetBalance(response.unwrap())`) expect(result).to.contain(`return Result.ok<${LibTypes.BigInt}, string>(decoded)`) expect(result).to.contain(`export class ${CONTRACT_NAME}Utils {`) expect(result).to.contain(`static encodeGetBalance(owner: Address): Bytes {`) @@ -907,7 +907,7 @@ describe('AbisInterfaceGenerator', () => { // Read function should call both helpers expect(result).to.contain(`const encodedData = ${CONTRACT_NAME}Utils.encodeGetValue()`) expect(result).to.contain(`if (response.isError) return Result.err<${LibTypes.BigInt}, string>(response.error)`) - expect(result).to.contain(`const decoded = ${CONTRACT_NAME}Utils.decodeGetValue(response.value)`) + expect(result).to.contain(`const decoded = ${CONTRACT_NAME}Utils.decodeGetValue(response.unwrap())`) expect(result).to.contain(`return Result.ok<${LibTypes.BigInt}, string>(decoded)`) // Write function should call encoded data helper diff --git a/packages/integration/tests/008-write-contract-calls-tests/src/task.ts b/packages/integration/tests/008-write-contract-calls-tests/src/task.ts index 2ac41253..56b7bc6b 100644 --- a/packages/integration/tests/008-write-contract-calls-tests/src/task.ts +++ b/packages/integration/tests/008-write-contract-calls-tests/src/task.ts @@ -14,16 +14,9 @@ export default function main(): void { const context = environment.getContext() - const userTokensResult = environment.relevantTokensQuery( - context.user, - [chainId], - USD.zero(), - [aUSDC], - ListType.AllowList - ) - if (userTokensResult.isError) throw new Error(userTokensResult.error) - - const userTokens = userTokensResult.value + const userTokens = environment + .relevantTokensQuery(context.user, [chainId], USD.zero(), [aUSDC], ListType.AllowList) + .unwrap() const feeUsdt = TokenAmount.fromStringDecimal(USDT, inputs.usdFeeAmount) diff --git a/packages/lib-ts/src/environment.ts b/packages/lib-ts/src/environment.ts index 32643a0e..0c115c1c 100644 --- a/packages/lib-ts/src/environment.ts +++ b/packages/lib-ts/src/environment.ts @@ -115,7 +115,7 @@ export namespace environment { if (pricesResult.isError) return Result.err(pricesResult.error) - const prices = pricesResult.value + const prices = pricesResult.unwrap() if (prices.length === 0) return Result.err('Prices not found for token ' + token.toString()) const sortedPrices = prices.sort((a: USD, b: USD) => a.compare(b)) @@ -173,7 +173,7 @@ export namespace environment { if (responseResult.isError) return Result.err(responseResult.error) - const response = responseResult.value + const response = responseResult.unwrap() const resultMap: Map = new Map() for (let i = 0; i < response.length; i++) { for (let j = 0; j < response[i].length; j++) { diff --git a/packages/lib-ts/src/tokens/ERC20Token.ts b/packages/lib-ts/src/tokens/ERC20Token.ts index 90eca9ab..bd49b7f5 100644 --- a/packages/lib-ts/src/tokens/ERC20Token.ts +++ b/packages/lib-ts/src/tokens/ERC20Token.ts @@ -164,7 +164,7 @@ export class ERC20Token extends BlockchainToken { this._symbol = ERC20Token.EMPTY_SYMBOL return } - this._symbol = evm.decode(new EvmDecodeParam('string', response.value)) + this._symbol = evm.decode(new EvmDecodeParam('string', response.unwrap())) } /** @@ -181,6 +181,6 @@ export class ERC20Token extends BlockchainToken { this._decimals = ERC20Token.EMPTY_DECIMALS return } - this._decimals = u8.parse(evm.decode(new EvmDecodeParam('uint8', result.value))) + this._decimals = u8.parse(evm.decode(new EvmDecodeParam('uint8', result.unwrap()))) } } diff --git a/packages/lib-ts/src/tokens/SPLToken.ts b/packages/lib-ts/src/tokens/SPLToken.ts index 0bcefb8b..36ba1278 100644 --- a/packages/lib-ts/src/tokens/SPLToken.ts +++ b/packages/lib-ts/src/tokens/SPLToken.ts @@ -102,7 +102,7 @@ export class SPLToken extends BlockchainToken { this._decimals = SPLToken.EMPTY_DECIMALS return } - const decimals = SvmMint.fromHex(result.value.accountsInfo[0].data).decimals + const decimals = SvmMint.fromHex(result.unwrap().accountsInfo[0].data).decimals this._decimals = decimals } @@ -120,12 +120,12 @@ export class SPLToken extends BlockchainToken { this._symbol = SPLToken.EMPTY_SYMBOL return } - const data = result.value.accountsInfo[0].data + const data = result.unwrap().accountsInfo[0].data // Return placeholder symbol from address if TokenMetadata standard is not used this._symbol = data === '0x' ? `${this.address.toString().slice(0, 5)}...${this.address.toString().slice(-5)}` - : SvmTokenMetadataData.fromTokenMetadataHex(result.value.accountsInfo[0].data).symbol + : SvmTokenMetadataData.fromTokenMetadataHex(result.unwrap().accountsInfo[0].data).symbol } /** diff --git a/packages/lib-ts/src/tokens/TokenAmount.ts b/packages/lib-ts/src/tokens/TokenAmount.ts index a09900a1..d292c745 100644 --- a/packages/lib-ts/src/tokens/TokenAmount.ts +++ b/packages/lib-ts/src/tokens/TokenAmount.ts @@ -222,7 +222,7 @@ export class TokenAmount { const tokenPriceResult = environment.tokenPriceQuery(this.token) if (tokenPriceResult.isError) return Result.err(tokenPriceResult.error) - const tokenPrice = tokenPriceResult.value + const tokenPrice = tokenPriceResult.unwrap() const amountUsd = this.amount.times(tokenPrice.value).downscale(this.decimals) return Result.ok(USD.fromBigInt(amountUsd)) } @@ -237,7 +237,7 @@ export class TokenAmount { const usdResult = this.toUsd() if (usdResult.isError) return Result.err(usdResult.error) - return usdResult.value.toTokenAmount(other) + return usdResult.unwrap().toTokenAmount(other) } /** diff --git a/packages/lib-ts/src/tokens/USD.ts b/packages/lib-ts/src/tokens/USD.ts index 5d839533..634f491f 100644 --- a/packages/lib-ts/src/tokens/USD.ts +++ b/packages/lib-ts/src/tokens/USD.ts @@ -197,7 +197,7 @@ export class USD { const tokenPriceResult = environment.tokenPriceQuery(token) if (tokenPriceResult.isError) return Result.err(tokenPriceResult.error) - const tokenPrice = tokenPriceResult.value + const tokenPrice = tokenPriceResult.unwrap() const tokenAmount = this.value.upscale(token.decimals).div(tokenPrice.value) return Result.ok(TokenAmount.fromBigInt(token, tokenAmount)) } diff --git a/packages/lib-ts/src/types/Result.ts b/packages/lib-ts/src/types/Result.ts index d2cd065b..032a90a0 100644 --- a/packages/lib-ts/src/types/Result.ts +++ b/packages/lib-ts/src/types/Result.ts @@ -4,20 +4,22 @@ // Copyright (c) 2018 Graph Protocol, Inc. and contributors. // Modified by Mimic Protocol, 2025. +import { Stringable } from '../helpers' + /** * The result of an operation, with a corresponding value and error type. */ -export class Result { +export class Result { constructor( private _value: Wrapped | null, private _error: Wrapped | null ) {} - static ok(value: V): Result { + static ok(value: V): Result { return new Result(new Wrapped(value), null) } - static err(error: E): Result { + static err(error: E): Result { return new Result(null, new Wrapped(error)) } @@ -29,15 +31,25 @@ export class Result { return this._error !== null } - get value(): V { - if (this.isError) throw new Error('Trying to get a value from an error result') - return changetype>(this._value).inner - } - get error(): E { if (this.isOk) throw new Error('Trying to get an error from a successful result') return changetype>(this._error).inner } + + unwrap(): V { + if (this.isError) throw new Error(this.error.toString()) + return changetype>(this._value).inner + } + + unwrapOr(defaultValue: V): V { + if (this.isError) return defaultValue + return this.unwrap() + } + + unwrapOrElse(defaultValue: () => V): V { + if (this.isError) return defaultValue() + return this.unwrap() + } } // This is used to wrap a generic so that it can be unioned with `null`, working around limitations diff --git a/packages/lib-ts/tests/tokens/TokenAmount.spec.ts b/packages/lib-ts/tests/tokens/TokenAmount.spec.ts index 91716ce3..0855aaa5 100644 --- a/packages/lib-ts/tests/tokens/TokenAmount.spec.ts +++ b/packages/lib-ts/tests/tokens/TokenAmount.spec.ts @@ -436,7 +436,7 @@ describe('TokenAmount', () => { const tokenAmount = TokenAmount.fromI32(randomERC20Token(), 0) const result = tokenAmount.toUsd() expect(result.isOk).toBe(true) - expect(result.value.toString()).toBe('0') + expect(result.unwrap().toString()).toBe('0') }) }) @@ -452,7 +452,7 @@ describe('TokenAmount', () => { const result = tokenAmount.toUsd() const expectedAmount = decimalTokenAmount * price expect(result.isOk).toBe(true) - expect(result.value.toString()).toBe(expectedAmount.toString()) + expect(result.unwrap().toString()).toBe(expectedAmount.toString()) }) it('converts correctly for a token with standard decimals', () => { @@ -466,7 +466,7 @@ describe('TokenAmount', () => { const result = tokenAmount.toUsd() const expectedAmount = decimalTokenAmount * price expect(result.isOk).toBe(true) - expect(result.value.toString()).toBe(expectedAmount.toString()) + expect(result.unwrap().toString()).toBe(expectedAmount.toString()) }) it('converts correctly for a token with more than standard decimals', () => { @@ -480,7 +480,7 @@ describe('TokenAmount', () => { const result = tokenAmount.toUsd() const expectedAmount = decimalTokenAmount * price expect(result.isOk).toBe(true) - expect(result.value.toString()).toBe(expectedAmount.toString()) + expect(result.unwrap().toString()).toBe(expectedAmount.toString()) }) }) }) @@ -491,7 +491,7 @@ describe('TokenAmount', () => { const tokenAmount = TokenAmount.fromI32(DenominationToken.USD(), 0) const result = tokenAmount.toUsd() expect(result.isOk).toBe(true) - expect(result.value.toString()).toBe('0') + expect(result.unwrap().toString()).toBe('0') }) }) @@ -502,7 +502,7 @@ describe('TokenAmount', () => { const result = tokenAmount.toUsd() expect(result.isOk).toBe(true) - expect(result.value.toString()).toBe(decimalTokenAmount.toString()) + expect(result.unwrap().toString()).toBe(decimalTokenAmount.toString()) }) }) }) @@ -513,7 +513,7 @@ describe('TokenAmount', () => { const tokenAmount = TokenAmount.fromI32(DenominationToken.USD(), 0) const result = tokenAmount.toUsd() expect(result.isOk).toBe(true) - expect(result.value.toString()).toBe('0') + expect(result.unwrap().toString()).toBe('0') }) }) diff --git a/packages/lib-ts/tests/tokens/USD.spec.ts b/packages/lib-ts/tests/tokens/USD.spec.ts index 171a8c2a..b45ed306 100644 --- a/packages/lib-ts/tests/tokens/USD.spec.ts +++ b/packages/lib-ts/tests/tokens/USD.spec.ts @@ -358,7 +358,7 @@ describe('USD', () => { const result = usdAmount.toTokenAmount(token) expect(result.isOk).toBe(true) - expect(result.value.amount.toString()).toBe('0') + expect(result.unwrap().amount.toString()).toBe('0') }) }) @@ -374,7 +374,7 @@ describe('USD', () => { const expectedAmount = BigInt.fromI32(decimalAmountUsd / price) expect(result.isOk).toBe(true) - expect(result.value.amount.toString()).toBe(zeroPadded(expectedAmount, tokenDecimals)) + expect(result.unwrap().amount.toString()).toBe(zeroPadded(expectedAmount, tokenDecimals)) }) it('converts correctly for a token with standard decimals', () => { @@ -388,7 +388,7 @@ describe('USD', () => { const expectedAmount = BigInt.fromI32(decimalAmountUsd / price) expect(result.isOk).toBe(true) - expect(result.value.amount.toString()).toBe(zeroPadded(expectedAmount, tokenDecimals)) + expect(result.unwrap().amount.toString()).toBe(zeroPadded(expectedAmount, tokenDecimals)) }) it('converts correctly for a token with more than standard decimals', () => { @@ -402,7 +402,7 @@ describe('USD', () => { const expectedAmount = BigInt.fromI32(decimalAmountUsd / price) expect(result.isOk).toBe(true) - expect(result.value.amount.toString()).toBe(zeroPadded(expectedAmount, tokenDecimals)) + expect(result.unwrap().amount.toString()).toBe(zeroPadded(expectedAmount, tokenDecimals)) }) }) }) diff --git a/packages/lib-ts/tests/types/Result.spec.ts b/packages/lib-ts/tests/types/Result.spec.ts new file mode 100644 index 00000000..02657d13 --- /dev/null +++ b/packages/lib-ts/tests/types/Result.spec.ts @@ -0,0 +1,123 @@ +import { Stringable } from '../../src/helpers' +import { Result } from '../../src/types' + +class TestError implements Stringable { + constructor(private readonly message: string) {} + + toString(): string { + return this.message + } +} + +let fallbackCalls: i32 = 0 +const fallback: () => i32 = () => ++fallbackCalls + +beforeEach(() => { + fallbackCalls = 0 +}) + +describe('Result', () => { + describe('ok', () => { + describe('when a value is provided', () => { + it('creates a successful result', () => { + const result = Result.ok(42) + + expect(result.isOk).toBe(true) + expect(result.isError).toBe(false) + expect(result.unwrap()).toBe(42) + }) + }) + }) + + describe('err', () => { + describe('when an error is provided', () => { + it('creates an error result', () => { + const error = new TestError('error message') + const result = Result.err(error) + + expect(result.isOk).toBe(false) + expect(result.isError).toBe(true) + expect(result.error.toString()).toBe(error.toString()) + }) + }) + }) + + describe('unwrap', () => { + describe('when result is successful', () => { + it('returns the contained value', () => { + const result = Result.ok('value') + + expect(result.unwrap()).toBe('value') + }) + }) + + describe('when result is an error', () => { + it('throws the error string', () => { + expect(() => { + Result.err(new TestError('error message')).unwrap() + }).toThrow('error message') + }) + }) + }) + + describe('unwrapOr', () => { + describe('when result is successful', () => { + it('returns the contained value', () => { + const result = Result.ok('value') + + expect(result.unwrapOr('default')).toBe('value') + }) + }) + + describe('when result is an error', () => { + it('returns the provided default value', () => { + const result = Result.err(new TestError('error message')) + + expect(result.unwrapOr('default')).toBe('default') + }) + }) + }) + + describe('unwrapOrElse', () => { + describe('when result is successful', () => { + it('returns the contained value without calling the fallback', () => { + const result = Result.ok(7) + + const value = result.unwrapOrElse(fallback) + + expect(value).toBe(7) + expect(fallbackCalls).toBe(0) + }) + }) + + describe('when result is an error', () => { + it('calls the fallback and returns its value', () => { + const result = Result.err(new TestError('error message')) + + const value = result.unwrapOrElse(fallback) + + expect(value).toBe(1) + expect(fallbackCalls).toBe(1) + }) + }) + }) + + describe('error getter', () => { + describe('when result is successful', () => { + it('throws when accessing the error value', () => { + expect(() => { + Result.ok('value').error + }).toThrow('Trying to get an error from a successful result') + }) + }) + + describe('when result is an error', () => { + it('returns the contained error', () => { + const error = new TestError('error message') + const result = Result.err(error) + + expect(result.error.toString()).toBe(error.toString()) + }) + }) + }) +})