Skip to content

Commit

Permalink
SEP-38: update SEP-38 tests based on stellar/stellar-protocol#1204 (#86)
Browse files Browse the repository at this point in the history
### What

Update SEP-38 tests based on stellar/stellar-protocol#1204. The changes include:
* SEP-38 `GET /price` and `POST /quote` now require the mandatory `context` request parameter.
* SEP-38 `GET /price` and `GET|POST /quote` now return the mandatory response parameters `total_price` and `fee`.
* The updated formulas from [SEP38#price-formulas](https://github.com/stellar/stellar-protocol/blob/faa99165050dcd44a9e0f700c3d019258d8b4321/ecosystem/sep-0038.md#price-formulas) are now being validated.
  • Loading branch information
marcelosalloum authored Jun 3, 2022
1 parent 8d43165 commit faec7ae
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 55 deletions.
2 changes: 1 addition & 1 deletion @stellar/anchor-tests/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stellar/anchor-tests",
"version": "0.4.1",
"version": "0.5.0",
"description": "stellar-anchor-tests is a library and command line interface for testing Stellar anchors.",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
Expand Down
43 changes: 42 additions & 1 deletion @stellar/anchor-tests/src/schemas/sep38.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,47 @@ export const pricesSchema = {
required: ["buy_assets"],
};

const rateFeeSchema = {
type: "object",
properties: {
total: {
type: "string"
},
asset: {
type: "string"
},
details: {
type: "array",
items: {
type: "object",
properties: {
name: {
type: "string"
},
description: {
type: "string"
},
amount: {
type: "string"
},
},
required: ["name", "amount"],
}
}
},
required: ["total", "asset"]
}

export const priceSchema = {
type: "object",
properties: {
price: {
type: "string",
},
total_price: {
type: "string",
},
fee: rateFeeSchema,
sell_amount: {
type: "string",
},
Expand All @@ -90,7 +125,7 @@ export const priceSchema = {
},
},
additionalProperties: false,
required: ["price", "sell_amount", "buy_amount"],
required: ["price", "total_price", "sell_amount", "buy_amount", "fee"],
};

export const quoteSchema = {
Expand All @@ -106,6 +141,10 @@ export const quoteSchema = {
price: {
type: "string",
},
total_price: {
type: "string",
},
fee: rateFeeSchema,
sell_asset: {
type: "string",
},
Expand All @@ -124,6 +163,8 @@ export const quoteSchema = {
"id",
"expires_at",
"price",
"total_price",
"fee",
"sell_asset",
"buy_asset",
"sell_amount",
Expand Down
76 changes: 51 additions & 25 deletions @stellar/anchor-tests/src/tests/sep38/postQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const requiresJwt: Test = {
sell_asset: this.context.expects.sep38StellarAsset,
buy_asset: this.context.expects.sep38OffChainAsset,
sell_amount: "100",
context: "sep31",
};
if (this.context.expects.sep38BuyDeliveryMethod)
requestBody.buy_delivery_method =
Expand Down Expand Up @@ -162,6 +163,7 @@ export const canCreateQuote: Test = {
sell_asset: this.context.expects.sep38StellarAsset,
buy_asset: this.context.expects.sep38OffChainAsset,
sell_amount: "100",
context: "sep31",
};
if (this.context.expects.sep38OffChainAssetBuyDeliveryMethod !== undefined)
requestBody.buy_delivery_method =
Expand Down Expand Up @@ -243,41 +245,65 @@ export const amountsAreValid: Test = {
INVALID_AMOUNTS: {
name: "amounts and price don't match",
text(args: any): string {
return `The amounts returned in the response do not add up. ${args.buyAmount} * ${args.price} != ${args.sellAmount}`;
return `The amounts returned in the response do not add up. ${args.message}`;
},
links: {
"POST /quote Response":
"https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md#response-3",
"Price formulas":
"https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md#price-formulas",
},
},
...genericFailures,
},
async run(_config: Config): Promise<Result> {
const result: Result = { networkCalls: [] };
const roundingMultiplier = Math.pow(
10,
Number(this.context.expects.sep38OffChainAssetDecimals),
);
if (
Math.round(
(Number(this.context.expects.sep38QuoteResponseObj.sell_amount) /
Number(this.context.expects.sep38QuoteResponseObj.price)) *
roundingMultiplier,
) /
roundingMultiplier !==
Math.round(
Number(this.context.expects.sep38QuoteResponseObj.buy_amount) *
roundingMultiplier,
) /
roundingMultiplier
) {
result.failure = makeFailure(this.failureModes.INVALID_AMOUNTS, {
buyAmount: this.context.expects.sep38QuoteResponseObj.buy_amount,
sellAmount: this.context.expects.sep38QuoteResponseObj.sell_amount,
price: this.context.expects.sep38QuoteResponseObj.price,
});
const decimals = Number(this.context.expects.sep38OffChainAssetDecimals);
const roundingMultiplier = Math.pow(10, decimals);

// validate total_price
// sell_amount / total_price = buy_amount
const sellAmount = Number(this.context.expects.sep38QuoteResponseObj.sell_amount);
const buyAmount = Number(this.context.expects.sep38QuoteResponseObj.buy_amount);
const totalPrice = Number(this.context.expects.sep38QuoteResponseObj.total_price)
const totalPriceMatchesAmounts =
Math.round((sellAmount / totalPrice) * roundingMultiplier) / roundingMultiplier
=== Math.round(buyAmount * roundingMultiplier) / roundingMultiplier;
if (!totalPriceMatchesAmounts) {
var message = `\nFormula "sell_amount = buy_amount * total_price" is not true for the number of decimals (${decimals}) required:`
message += `\n\t${sellAmount} != ${buyAmount} * ${totalPrice}`
result.failure = makeFailure(this.failureModes.INVALID_AMOUNTS, { message });
return result;
}

// validate price
const sellAsset = this.context.expects.sep38QuoteResponseObj.sell_asset;
const buyAsset = this.context.expects.sep38QuoteResponseObj.buy_asset;
const feeAsset = this.context.expects.sep38QuoteResponseObj.fee.asset;
const price = this.context.expects.sep38QuoteResponseObj.price;
const feeTotal = this.context.expects.sep38QuoteResponseObj.fee.total;
if (feeAsset === sellAsset) {
// sell_amount - fee = price * buy_amount // when `fee` is in `sell_asset`
const priceAndFeeMatchAmounts =
Math.round((sellAmount - feeTotal) * roundingMultiplier) / roundingMultiplier
=== Math.round(price * buyAmount * roundingMultiplier) / roundingMultiplier;
if (!priceAndFeeMatchAmounts) {
var message = `\nFormula "sell_amount - fee = price * buy_amount" is not true for the number of decimals (${decimals}) required:`
message += `\n\t${sellAmount} - ${feeTotal} != ${price} * ${buyAmount}`
result.failure = makeFailure(this.failureModes.INVALID_AMOUNTS, { message });
return result;
}
} else if (feeAsset === buyAsset) {
// sell_amount / price = buy_amount + fee // when `fee` is in `buy_asset`
const priceAndFeeMatchAmounts =
Math.round((sellAmount / price) * roundingMultiplier) / roundingMultiplier
=== Math.round((buyAmount + feeTotal) * roundingMultiplier) / roundingMultiplier;
if (!priceAndFeeMatchAmounts) {
var message = `\nFormula "sell_amount / price = (buy_amount + fee)" is not true for the number of decimals (${decimals}) required:`
message += `\n\t${sellAmount} / ${price} != ${buyAmount} + ${feeTotal}`
result.failure = makeFailure(this.failureModes.INVALID_AMOUNTS, { message });
return result;
}
}

return result;
},
};
Expand Down
102 changes: 77 additions & 25 deletions @stellar/anchor-tests/src/tests/sep38/price.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export const returnsValidResponse: Test = {
sep38SellAmount: undefined,
sep38BuyAmount: undefined,
sep38Price: undefined,
sep38TotalPrice: undefined,
sep38FeeTotal: undefined,
sep38FeeAsset: undefined,
},
},
failureModes: {
Expand Down Expand Up @@ -78,6 +81,7 @@ export const returnsValidResponse: Test = {
sell_asset: this.context.expects.sep38StellarAsset,
buy_asset: this.context.expects.sep38OffChainAsset,
sell_amount: "100",
context: "sep31",
};
if (this.context.expects.sep38OffChainAssetBuyDeliveryMethod !== undefined)
requestBody.buy_delivery_method =
Expand All @@ -101,6 +105,7 @@ export const returnsValidResponse: Test = {
result,
"application/json",
);

if (!priceResponse) return result;
const validationResult = validate(priceResponse, priceSchema);
if (validationResult.errors.length !== 0) {
Expand All @@ -109,17 +114,31 @@ export const returnsValidResponse: Test = {
});
return result;
}

if (!priceResponse.fee || !priceResponse.fee.asset) {
result.failure = makeFailure(this.failureModes.INVALID_SCHEMA, {
errors: "The response body from GET /price does not contain a fee asset.",
});
return result;
}

if (
!Number(priceResponse.buy_amount) ||
!Number(priceResponse.sell_amount) ||
!Number(priceResponse.price)
!Number(priceResponse.price) ||
!Number(priceResponse.total_price) ||
!Number(priceResponse.fee.total)
) {
result.failure = makeFailure(this.failureModes.INVALID_NUMBER);
return result;
}

this.context.provides.sep38SellAmount = Number(priceResponse.sell_amount);
this.context.provides.sep38BuyAmount = Number(priceResponse.buy_amount);
this.context.provides.sep38Price = Number(priceResponse.price);
this.context.provides.sep38TotalPrice = Number(priceResponse.total_price);
this.context.provides.sep38FeeTotal = Number(priceResponse.fee.total);
this.context.provides.sep38FeeAsset = priceResponse.fee.asset;
return result;
},
};
Expand All @@ -134,48 +153,79 @@ export const amountsAreValid: Test = {
sep38SellAmount: undefined,
sep38BuyAmount: undefined,
sep38Price: undefined,
sep38TotalPrice: undefined,
sep38FeeTotal: undefined,
sep38FeeAsset: undefined,
sep38OffChainAssetDecimals: undefined,
sep38StellarAsset: undefined, // sell_asset
sep38OffChainAsset: undefined, // buy_asset
},
provides: {},
},
failureModes: {
INVALID_AMOUNTS: {
name: "amounts and price don't match",
text(args: any): string {
return `The amounts returned in the response do not add up. ${args.buyAmount} * ${args.price} != ${args.sellAmount}`;
return `The amounts returned in the response do not add up. ${args.message}`;
},
links: {
"GET /price Response":
"https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md#response-2",
"Price formulas":
"https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md#price-formulas",
},
},
...genericFailures,
},

async run(_config: Config): Promise<Result> {
const result: Result = { networkCalls: [] };
const roundingMultiplier = Math.pow(
10,
Number(this.context.expects.sep38OffChainAssetDecimals),
);
if (
Math.round(
(Number(this.context.expects.sep38SellAmount) /
Number(this.context.expects.sep38Price)) *
roundingMultiplier,
) /
roundingMultiplier !==
Math.round(
Number(this.context.expects.sep38BuyAmount) * roundingMultiplier,
) /
roundingMultiplier
) {
result.failure = makeFailure(this.failureModes.INVALID_AMOUNTS, {
buyAmount: this.context.expects.sep38BuyAmount,
sellAmount: this.context.expects.sep38SellAmount,
price: this.context.expects.sep38Price,
});
const decimals = Number(this.context.expects.sep38OffChainAssetDecimals);
const roundingMultiplier = Math.pow(10, decimals);

// validate total_price
// sell_amount / total_price = buy_amount
const sellAmount = Number(this.context.expects.sep38SellAmount);
const buyAmount = Number(this.context.expects.sep38BuyAmount);
const totalPrice = Number(this.context.expects.sep38TotalPrice)
const totalPriceMatchesAmounts =
Math.round((sellAmount / totalPrice) * roundingMultiplier) / roundingMultiplier
=== Math.round(buyAmount * roundingMultiplier) / roundingMultiplier;
if (!totalPriceMatchesAmounts) {
var message = `\nFormula "sell_amount = buy_amount * total_price" is not true for the number of decimals (${decimals}) required:`
message += `\n\t${sellAmount} != ${buyAmount} * ${totalPrice}`
result.failure = makeFailure(this.failureModes.INVALID_AMOUNTS, { message });
return result;
}

// validate price
const sellAsset = this.context.expects.sep38StellarAsset;
const buyAsset = this.context.expects.sep38OffChainAsset;
const feeAsset = this.context.expects.sep38FeeAsset;
const price = this.context.expects.sep38Price;
const feeTotal = this.context.expects.sep38FeeTotal;
if (feeAsset === sellAsset) {
// sell_amount - fee = price * buy_amount // when `fee` is in `sell_asset`
const priceAndFeeMatchAmounts =
Math.round((sellAmount - feeTotal) * roundingMultiplier) / roundingMultiplier
=== Math.round(price * buyAmount * roundingMultiplier) / roundingMultiplier;
if (!priceAndFeeMatchAmounts) {
var message = `\nFormula "sell_amount - fee = price * buy_amount" is not true for the number of decimals (${decimals}) required:`
message += `\n\t${sellAmount} - ${feeTotal} != ${price} * ${buyAmount}`
result.failure = makeFailure(this.failureModes.INVALID_AMOUNTS, { message });
return result;
}
} else if (feeAsset === buyAsset) {
// sell_amount / price = buy_amount + fee // when `fee` is in `buy_asset`
const priceAndFeeMatchAmounts =
Math.round((sellAmount / price) * roundingMultiplier) / roundingMultiplier
=== Math.round((buyAmount + feeTotal) * roundingMultiplier) / roundingMultiplier;
if (!priceAndFeeMatchAmounts) {
var message = `\nFormula "sell_amount / price = (buy_amount + fee)" is not true for the number of decimals (${decimals}) required:`
message += `\n\t${sellAmount} / ${price} != ${buyAmount} + ${feeTotal}`
result.failure = makeFailure(this.failureModes.INVALID_AMOUNTS, { message });
return result;
}
}

return result;
},
};
Expand Down Expand Up @@ -204,6 +254,7 @@ export const acceptsBuyAmounts: Test = {
sell_asset: this.context.expects.sep38StellarAsset,
buy_asset: this.context.expects.sep38OffChainAsset,
buy_amount: "100",
context: "sep31",
};
if (this.context.expects.sep38OffChainAssetBuyDeliveryMethod !== undefined)
requestBody.buy_delivery_method =
Expand Down Expand Up @@ -283,6 +334,7 @@ export const deliveryMethodIsOptional: Test = {
sell_asset: this.context.expects.sep38StellarAsset,
buy_asset: this.context.expects.sep38OffChainAsset,
sell_amount: "100",
context: "sep31",
};
const networkCall: NetworkCall = {
request: new Request(
Expand Down
6 changes: 5 additions & 1 deletion @stellar/anchor-tests/src/tests/sep38/prices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,11 @@ export const hasValidSchema: Test = {
}
for (const asset of pricesResponse.buy_assets) {
const parts = asset.asset.split(":");
if (parts.length !== 2 || parts[0] !== "iso4217") {

const notValidFiat = parts.length === 2 && parts[0] !== "iso4217";
const notValidStellar = parts.length === 3 && parts[0] !== "stellar";
const notValidAtAll = parts.length < 2 || parts.length > 3;
if (notValidFiat || notValidStellar || notValidAtAll) {
result.failure = makeFailure(this.failureModes.INVALID_ASSET_VALUE, {
asset: asset.asset,
});
Expand Down
2 changes: 1 addition & 1 deletion @stellar/anchor-tests/src/tests/sep38/tests.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { default as tomlTests } from "./toml";
import { default as infoTests } from "./info";
import { default as priceTests } from "./price";
import { default as pricesTests } from "./prices";
import { default as priceTests } from "./price";
import { default as postQuoteTests } from "./postQuote";
import { default as getQuoteTests } from "./getQuote";

Expand Down
Loading

0 comments on commit faec7ae

Please sign in to comment.