diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a0b5058 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,72 @@ +name: 'Release' + +permissions: write-all + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version' + type: string + required: true + +jobs: + prepare: + runs-on: [ubuntu-22.04] + outputs: + version_tag: ${{ steps.version_tag.outputs.value }} + build_date: ${{ steps.build_date.outputs.value }} + steps: + - name: Check branch + if: ${{ !startsWith(github.ref, 'refs/heads/release') }} + run: | + echo "This workflow should be triggered with workflow_dispatch on release branch" + exit 1 + - name: Format version tag + shell: bash + id: version_tag + env: + INPUT_TAG: ${{ github.event.inputs.version }} + run: | + TAG=${INPUT_TAG#v} + echo "::set-output name=value::v$TAG" + - name: Build date + shell: bash + id: build_date + run: echo "::set-output name=value::$(date +%FT%T%z)" + + release: + needs: + - prepare + runs-on: [ ubuntu-22.04 ] + env: + VERSION_TAG: ${{ needs.prepare.outputs.version_tag }} + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: "1.21.x" + + - name: Setup Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Create tag + run: | + git tag -d "$VERSION_TAG" 2> /dev/null || echo "Release tag '$VERSION_TAG' does NOT exist" + git tag --annotate --message "pancake-v3-sdk $VERSION_TAG" "$VERSION_TAG" + git push origin "refs/tags/$VERSION_TAG" + + - name: Create release + uses: softprops/action-gh-release@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + tag_name: ${{ env.VERSION_TAG }} + prerelease: false + name: "Pancake V3 SDK ${{ env.VERSION_TAG }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 76dac0e..65066b5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,8 +4,8 @@ jobs: test: strategy: matrix: - go-version: [1.20.x] - os: [ubuntu-latest, macos-latest, windows-latest] + go-version: [1.18.x] + os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - name: Install Go @@ -23,7 +23,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.20.x + go-version: 1.18.x - name: Checkout code uses: actions/checkout@v2 - uses: actions/cache@v2 diff --git a/entities/pool.go b/entities/pool.go index b1dbd66..13b00b4 100644 --- a/entities/pool.go +++ b/entities/pool.go @@ -12,9 +12,9 @@ import ( ) var ( - ErrFeeTooHigh = errors.New("Fee too high") - ErrInvalidSqrtRatioX96 = errors.New("Invalid sqrtRatioX96") - ErrTokenNotInvolved = errors.New("Token not involved in pool") + ErrFeeTooHigh = errors.New("fee too high") + ErrInvalidSqrtRatioX96 = errors.New("invalid sqrtRatioX96") + ErrTokenNotInvolved = errors.New("token not involved in pool") ErrSqrtPriceLimitX96TooLow = errors.New("SqrtPriceLimitX96 too low") ErrSqrtPriceLimitX96TooHigh = errors.New("SqrtPriceLimitX96 too high") ) @@ -43,6 +43,29 @@ type Pool struct { token1Price *entities.Price } +type SwapResult struct { + amountCalculated *big.Int + sqrtRatioX96 *big.Int + liquidity *big.Int + remainingTargetAmount *big.Int + currentTick int + crossInitTickLoops int +} + +type GetOutputAmountResult struct { + ReturnedAmount *entities.CurrencyAmount + RemainingAmountIn *entities.CurrencyAmount + NewPoolState *Pool + CrossInitTickLoops int +} + +type GetInputAmountResult struct { + ReturnedAmount *entities.CurrencyAmount + RemainingAmountOut *entities.CurrencyAmount + NewPoolState *Pool + CrossInitTickLoops int +} + func GetAddress(tokenA, tokenB *entities.Token, fee constants.FeeAmount, initCodeHashManualOverride string) (common.Address, error) { return utils.ComputePoolAddress(constants.FactoryAddress, tokenA, tokenB, fee, initCodeHashManualOverride) } @@ -149,14 +172,14 @@ func (p *Pool) ChainID() uint { * @param sqrtPriceLimitX96 The Q64.96 sqrt price limit * @returns The output amount and the pool with updated state */ -func (p *Pool) GetOutputAmount(inputAmount *entities.CurrencyAmount, sqrtPriceLimitX96 *big.Int) (*entities.CurrencyAmount, *Pool, error) { +func (p *Pool) GetOutputAmount(inputAmount *entities.CurrencyAmount, sqrtPriceLimitX96 *big.Int) (*GetOutputAmountResult, error) { if !(inputAmount.Currency.IsToken() && p.InvolvesToken(inputAmount.Currency.Wrapped())) { - return nil, nil, ErrTokenNotInvolved + return nil, ErrTokenNotInvolved } zeroForOne := inputAmount.Currency.Equal(p.Token0) - outputAmount, sqrtRatioX96, liquidity, tickCurrent, err := p.swap(zeroForOne, inputAmount.Quotient(), sqrtPriceLimitX96) + swapResult, err := p.swap(zeroForOne, inputAmount.Quotient(), sqrtPriceLimitX96) if err != nil { - return nil, nil, err + return nil, err } var outputToken *entities.Token if zeroForOne { @@ -164,11 +187,25 @@ func (p *Pool) GetOutputAmount(inputAmount *entities.CurrencyAmount, sqrtPriceLi } else { outputToken = p.Token0 } - pool, err := NewPool(p.Token0, p.Token1, p.Fee, sqrtRatioX96, liquidity, tickCurrent, p.TickDataProvider) + pool, err := NewPool( + p.Token0, + p.Token1, + p.Fee, + swapResult.sqrtRatioX96, + swapResult.liquidity, + swapResult.currentTick, + p.TickDataProvider, + ) if err != nil { - return nil, nil, err + return nil, err } - return entities.FromRawAmount(outputToken, new(big.Int).Mul(outputAmount, constants.NegativeOne)), pool, nil + + return &GetOutputAmountResult{ + ReturnedAmount: entities.FromRawAmount(outputToken, new(big.Int).Mul(swapResult.amountCalculated, constants.NegativeOne)), + RemainingAmountIn: entities.FromRawAmount(inputAmount.Currency, swapResult.remainingTargetAmount), + NewPoolState: pool, + CrossInitTickLoops: swapResult.crossInitTickLoops, + }, nil } /** @@ -177,14 +214,14 @@ func (p *Pool) GetOutputAmount(inputAmount *entities.CurrencyAmount, sqrtPriceLi * @param sqrtPriceLimitX96 The Q64.96 sqrt price limit. If zero for one, the price cannot be less than this value after the swap. If one for zero, the price cannot be greater than this value after the swap * @returns The input amount and the pool with updated state */ -func (p *Pool) GetInputAmount(outputAmount *entities.CurrencyAmount, sqrtPriceLimitX96 *big.Int) (*entities.CurrencyAmount, *Pool, error) { +func (p *Pool) GetInputAmount(outputAmount *entities.CurrencyAmount, sqrtPriceLimitX96 *big.Int) (*GetInputAmountResult, error) { if !(outputAmount.Currency.IsToken() && p.InvolvesToken(outputAmount.Currency.Wrapped())) { - return nil, nil, ErrTokenNotInvolved + return nil, ErrTokenNotInvolved } zeroForOne := outputAmount.Currency.Equal(p.Token1) - inputAmount, sqrtRatioX96, liquidity, tickCurrent, err := p.swap(zeroForOne, new(big.Int).Mul(outputAmount.Quotient(), constants.NegativeOne), sqrtPriceLimitX96) + swapResult, err := p.swap(zeroForOne, new(big.Int).Mul(outputAmount.Quotient(), constants.NegativeOne), sqrtPriceLimitX96) if err != nil { - return nil, nil, err + return nil, err } var inputToken *entities.Token if zeroForOne { @@ -192,11 +229,25 @@ func (p *Pool) GetInputAmount(outputAmount *entities.CurrencyAmount, sqrtPriceLi } else { inputToken = p.Token1 } - pool, err := NewPool(p.Token0, p.Token1, p.Fee, sqrtRatioX96, liquidity, tickCurrent, p.TickDataProvider) + pool, err := NewPool( + p.Token0, + p.Token1, + p.Fee, + swapResult.sqrtRatioX96, + swapResult.liquidity, + swapResult.currentTick, + p.TickDataProvider, + ) if err != nil { - return nil, nil, err + return nil, err } - return entities.FromRawAmount(inputToken, inputAmount), pool, nil + + return &GetInputAmountResult{ + ReturnedAmount: entities.FromRawAmount(inputToken, swapResult.amountCalculated), + RemainingAmountOut: entities.FromRawAmount(outputAmount.Currency, swapResult.remainingTargetAmount), + NewPoolState: pool, + CrossInitTickLoops: swapResult.crossInitTickLoops, + }, nil } /** @@ -204,12 +255,14 @@ func (p *Pool) GetInputAmount(outputAmount *entities.CurrencyAmount, sqrtPriceLi * @param zeroForOne Whether the amount in is token0 or token1 * @param amountSpecified The amount of the swap, which implicitly configures the swap as exact input (positive), or exact output (negative) * @param sqrtPriceLimitX96 The Q64.96 sqrt price limit. If zero for one, the price cannot be less than this value after the swap. If one for zero, the price cannot be greater than this value after the swap - * @returns amountCalculated - * @returns sqrtRatioX96 - * @returns liquidity - * @returns tickCurrent + * @returns swapResult.amountCalculated + * @returns swapResult.sqrtRatioX96 + * @returns swapResult.liquidity + * @returns swapResult.tickCurrent */ -func (p *Pool) swap(zeroForOne bool, amountSpecified, sqrtPriceLimitX96 *big.Int) (amountCalCulated *big.Int, sqrtRatioX96 *big.Int, liquidity *big.Int, tickCurrent int, err error) { +func (p *Pool) swap(zeroForOne bool, amountSpecified, sqrtPriceLimitX96 *big.Int) (*SwapResult, error) { + var err error + if sqrtPriceLimitX96 == nil { if zeroForOne { sqrtPriceLimitX96 = new(big.Int).Add(utils.MinSqrtRatio, constants.One) @@ -220,17 +273,17 @@ func (p *Pool) swap(zeroForOne bool, amountSpecified, sqrtPriceLimitX96 *big.Int if zeroForOne { if sqrtPriceLimitX96.Cmp(utils.MinSqrtRatio) < 0 { - return nil, nil, nil, 0, ErrSqrtPriceLimitX96TooLow + return nil, ErrSqrtPriceLimitX96TooLow } if sqrtPriceLimitX96.Cmp(p.SqrtRatioX96) >= 0 { - return nil, nil, nil, 0, ErrSqrtPriceLimitX96TooHigh + return nil, ErrSqrtPriceLimitX96TooHigh } } else { if sqrtPriceLimitX96.Cmp(utils.MaxSqrtRatio) > 0 { - return nil, nil, nil, 0, ErrSqrtPriceLimitX96TooHigh + return nil, ErrSqrtPriceLimitX96TooHigh } if sqrtPriceLimitX96.Cmp(p.SqrtRatioX96) <= 0 { - return nil, nil, nil, 0, ErrSqrtPriceLimitX96TooLow + return nil, ErrSqrtPriceLimitX96TooLow } } @@ -252,6 +305,10 @@ func (p *Pool) swap(zeroForOne bool, amountSpecified, sqrtPriceLimitX96 *big.Int liquidity: p.Liquidity, } + // crossInitTickLoops is the number of loops that cross an initialized tick. + // We only count when tick passes an initialized tick, since gas only significant in this case. + crossInitTickLoops := 0 + // start swap while loop for state.amountSpecifiedRemaining.Cmp(constants.Zero) != 0 && state.sqrtPriceX96.Cmp(sqrtPriceLimitX96) != 0 { var step StepComputations @@ -262,7 +319,7 @@ func (p *Pool) swap(zeroForOne bool, amountSpecified, sqrtPriceLimitX96 *big.Int // tickBitmap.nextInitializedTickWithinOneWord step.tickNext, step.initialized, err = p.TickDataProvider.NextInitializedTickIndex(state.tick, zeroForOne) if err != nil { - return nil, nil, nil, 0, err + return nil, err } if step.tickNext < utils.MinTick { @@ -273,7 +330,7 @@ func (p *Pool) swap(zeroForOne bool, amountSpecified, sqrtPriceLimitX96 *big.Int step.sqrtPriceNextX96, err = utils.GetSqrtRatioAtTick(step.tickNext) if err != nil { - return nil, nil, nil, 0, err + return nil, err } var targetValue *big.Int if zeroForOne { @@ -292,7 +349,7 @@ func (p *Pool) swap(zeroForOne bool, amountSpecified, sqrtPriceLimitX96 *big.Int state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount, err = utils.ComputeSwapStep(state.sqrtPriceX96, targetValue, state.liquidity, state.amountSpecifiedRemaining, p.Fee) if err != nil { - return nil, nil, nil, 0, err + return nil, err } if exactInput { @@ -309,7 +366,7 @@ func (p *Pool) swap(zeroForOne bool, amountSpecified, sqrtPriceLimitX96 *big.Int if step.initialized { tick, err := p.TickDataProvider.GetTick(step.tickNext) if err != nil { - return nil, nil, nil, 0, err + return nil, err } liquidityNet := tick.LiquidityNet @@ -319,6 +376,8 @@ func (p *Pool) swap(zeroForOne bool, amountSpecified, sqrtPriceLimitX96 *big.Int liquidityNet = new(big.Int).Mul(liquidityNet, constants.NegativeOne) } state.liquidity = utils.AddDelta(state.liquidity, liquidityNet) + + crossInitTickLoops++ } if zeroForOne { state.tick = step.tickNext - 1 @@ -329,11 +388,19 @@ func (p *Pool) swap(zeroForOne bool, amountSpecified, sqrtPriceLimitX96 *big.Int // recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't moved state.tick, err = utils.GetTickAtSqrtRatio(state.sqrtPriceX96) if err != nil { - return nil, nil, nil, 0, err + return nil, err } } } - return state.amountCalculated, state.sqrtPriceX96, state.liquidity, state.tick, nil + + return &SwapResult{ + amountCalculated: state.amountCalculated, + sqrtRatioX96: state.sqrtPriceX96, + liquidity: state.liquidity, + currentTick: state.tick, + remainingTargetAmount: state.amountSpecifiedRemaining, + crossInitTickLoops: crossInitTickLoops, + }, nil } func (p *Pool) tickSpacing() int { diff --git a/entities/pool_test.go b/entities/pool_test.go index 9583215..85e034f 100644 --- a/entities/pool_test.go +++ b/entities/pool_test.go @@ -14,7 +14,9 @@ import ( var ( USDC = entities.NewToken(1, common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), 6, "USDC", "USD Coin") + USDT = entities.NewToken(1, common.HexToAddress("0xdac17f958d2ee523a2206206994597c13d831ec7"), 6, "USDT", "Tether USD") DAI = entities.NewToken(1, common.HexToAddress("0x6B175474E89094C44Da98b954EedeAC495271d0F"), 18, "DAI", "Dai Stablecoin") + WETH = entities.NewToken(1, common.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"), 18, "WETH", "Wrapped Ether") OneEther = big.NewInt(1e18) ) @@ -138,26 +140,28 @@ func newTestPool() *Pool { } return pool } + func TestGetOutputAmount(t *testing.T) { pool := newTestPool() // USDC -> DAI inputAmount := entities.FromRawAmount(USDC, big.NewInt(100)) - outputAmount, _, err := pool.GetOutputAmount(inputAmount, nil) + getOutputAmountResult, err := pool.GetOutputAmount(inputAmount, nil) if err != nil { t.Fatal(err) } - assert.True(t, outputAmount.Currency.Equal(DAI)) - assert.Equal(t, outputAmount.Quotient(), big.NewInt(98)) + assert.True(t, getOutputAmountResult.ReturnedAmount.Currency.Equal(DAI)) + assert.Equal(t, getOutputAmountResult.ReturnedAmount.Quotient(), big.NewInt(98)) // DAI -> USDC inputAmount = entities.FromRawAmount(DAI, big.NewInt(100)) - outputAmount, _, err = pool.GetOutputAmount(inputAmount, nil) + getOutputAmountResult, err = pool.GetOutputAmount(inputAmount, nil) if err != nil { t.Fatal(err) } - assert.True(t, outputAmount.Currency.Equal(USDC)) - assert.Equal(t, outputAmount.Quotient(), big.NewInt(98)) + assert.True(t, getOutputAmountResult.ReturnedAmount.Currency.Equal(USDC)) + assert.Equal(t, getOutputAmountResult.ReturnedAmount.Quotient(), big.NewInt(98)) + assert.Equal(t, getOutputAmountResult.RemainingAmountIn.Quotient(), big.NewInt(0)) } func TestGetInputAmount(t *testing.T) { @@ -165,19 +169,20 @@ func TestGetInputAmount(t *testing.T) { // USDC -> DAI outputAmount := entities.FromRawAmount(DAI, big.NewInt(98)) - inputAmount, _, err := pool.GetInputAmount(outputAmount, nil) + getInputAmountResult, err := pool.GetInputAmount(outputAmount, nil) if err != nil { t.Fatal(err) } - assert.True(t, inputAmount.Currency.Equal(USDC)) - assert.Equal(t, inputAmount.Quotient(), big.NewInt(100)) + assert.True(t, getInputAmountResult.ReturnedAmount.Currency.Equal(USDC)) + assert.Equal(t, getInputAmountResult.ReturnedAmount.Quotient(), big.NewInt(100)) // DAI -> USDC outputAmount = entities.FromRawAmount(USDC, big.NewInt(98)) - inputAmount, _, err = pool.GetInputAmount(outputAmount, nil) + getInputAmountResult, err = pool.GetInputAmount(outputAmount, nil) if err != nil { t.Fatal(err) } - assert.True(t, inputAmount.Currency.Equal(DAI)) - assert.Equal(t, inputAmount.Quotient(), big.NewInt(100)) + assert.True(t, getInputAmountResult.ReturnedAmount.Currency.Equal(DAI)) + assert.Equal(t, getInputAmountResult.ReturnedAmount.Quotient(), big.NewInt(100)) + assert.Equal(t, getInputAmountResult.RemainingAmountOut.Quotient(), big.NewInt(0)) }