Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions oracles/redstone/sample-consumer/sample-consumer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import "@ton/test-utils"
import {SandboxContract, TreasuryContract, Blockchain} from "@ton/sandbox"
import {SampleConsumer} from "../../output/SampleConsumer_SampleConsumer"
import {
feedIdToBigInt,
getRedstoneSignedDataPackages,
getRedstoneSigners,
serializeSignedDataPackages,
serializeSigners,
signersToBigInt,
} from "../utils"
import {
SingleFeedMan,
storeReadPrice,
storeUpdatePrice,
UpdatePrice,
} from "../../output/SampleConsumer_SingleFeedMan"
import {beginCell, Cell, toNano} from "@ton/core"

describe("SampleConsumer tests", () => {
let blockchain: Blockchain
let deployer: SandboxContract<TreasuryContract>
let sampleConsumer: SandboxContract<SampleConsumer>
let singleFeedMan: SandboxContract<SingleFeedMan>

const feedId = "TON"

beforeAll(async () => {
blockchain = await Blockchain.create()
deployer = await blockchain.treasury("deployer")

const signers = getRedstoneSigners()

singleFeedMan = blockchain.openContract(
await SingleFeedMan.fromInit(
feedIdToBigInt(feedId),
serializeSigners(signersToBigInt(signers)),
BigInt(Math.min(signers.length, 3)),
{
$$type: "PriceData",
timestamp: 0n,
price: 0n,
},
),
)

const signedDataPackages = await getRedstoneSignedDataPackages(feedId, signers)

const signedDataPackagesCell = serializeSignedDataPackages(signedDataPackages, signers)

const bodyStruct: UpdatePrice = {
$$type: "UpdatePrice",
feedId: feedIdToBigInt(feedId),
dataPackages: signedDataPackagesCell.asSlice(),
}

const res = await singleFeedMan.send(
deployer.getSender(),
{value: toNano("0.05")},
bodyStruct,
)

expect(res.transactions).toHaveTransaction({
from: deployer.address,
to: singleFeedMan.address,
success: true,
op: SingleFeedMan.opcodes.UpdatePrice,
outMessagesCount: 1,
body: beginCell().store(storeUpdatePrice(bodyStruct)).endCell(),
})

expect(res.transactions).toHaveTransaction({
from: singleFeedMan.address,
to: deployer.address,
op: SingleFeedMan.opcodes.PriceResponse,
})

sampleConsumer = blockchain.openContract(
await SampleConsumer.fromInit(
feedIdToBigInt(feedId),
{
$$type: "PriceData",
timestamp: 0n,
price: 0n,
},
singleFeedMan.address,
),
)
})

it("should deploy sample consumer and correctly fetch price", async () => {
const fetchPriceResult = await sampleConsumer.send(
deployer.getSender(),
{value: toNano("0.05"), bounce: false},
null,
)

expect(fetchPriceResult.transactions).toHaveTransaction({
from: deployer.address,
to: sampleConsumer.address,
op: undefined,
outMessagesCount: 1,
body: new Cell(),
success: true,
deploy: true,
})

expect(fetchPriceResult.transactions).toHaveTransaction({
from: sampleConsumer.address,
to: singleFeedMan.address,
op: SingleFeedMan.opcodes.ReadPrice,
body: beginCell()
.store(
storeReadPrice({
$$type: "ReadPrice",
feedId: feedIdToBigInt(feedId),
}),
)
.endCell(),
success: true,
outMessagesCount: 1,
})

expect(fetchPriceResult.transactions).toHaveTransaction({
from: singleFeedMan.address,
to: sampleConsumer.address,
op: SingleFeedMan.opcodes.PriceResponse,
success: true,
})

const lastFetchedPriceData = await sampleConsumer.getLastFetchedPriceData()
// console.log(lastFetchedPriceData);
expect(lastFetchedPriceData.price).toBeGreaterThan(0n)
expect(lastFetchedPriceData.timestamp).toBeGreaterThan(0n)
})
})
30 changes: 30 additions & 0 deletions oracles/redstone/sample-consumer/sample-consumer.tact
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import "../single-feed-man/single-feed-man";

contract SampleConsumer(
feedId: Int as uint256,
lastFetchedPriceData: PriceData,
priceFeed: Address,
) {
receive() {
message(MessageParameters {
value: 0,
to: self.priceFeed,
bounce: false,
mode: SendRemainingValue | SendBounceIfActionFail,
body: ReadPrice {
feedId: self.feedId,
}.toCell(),
});
}

receive(msg: PriceResponse) {
require(sender() == self.priceFeed, "Only price feed can send price response");
require(msg.feedId == self.feedId, "Feed ID mismatch");

self.lastFetchedPriceData = msg.priceData;
}

get fun lastFetchedPriceData(): PriceData {
return self.lastFetchedPriceData;
}
}
33 changes: 33 additions & 0 deletions oracles/redstone/single-feed-man/crypto.tact
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
struct EcrecoverResult {
pubKey: UncompressedPubKey;
success: Bool;
}

struct EcdsaSignature {
r: Int as uint256;
s: Int as uint256;
recoveryParam: Int as uint8;
}

struct UncompressedPubKey {
h: Int;
x: Int;
y: Int;
}

asm inline fun ecrecover(hash: Int, v: Int, r: Int, s: Int): EcrecoverResult { ECRECOVER NULLSWAPIFNOT NULLSWAPIFNOT2 }

asm extends inline fun keccak(self: Builder): Int { 1 INT HASHEXT_KECCAK256 }
asm extends inline fun keccak(self: Slice): Int { 1 INT HASHEXT_KECCAK256 }

inline fun recoverAddress(hash: Int, signature: EcdsaSignature): Int {
let result = ecrecover(hash, signature.recoveryParam, signature.r, signature.s);

if (!result.success) {
return 0;
}

let builder = beginCell().storeUint(result.pubKey.x, 256).storeUint(result.pubKey.y, 256);

return builder.keccak() & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // mask to 20 bytes
}
120 changes: 120 additions & 0 deletions oracles/redstone/single-feed-man/fake-tuple.tact
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import "./utils";

// only for 1-6 elements
struct FakeTuple {
dontTouchMyPizza: Cell; // it's not a cell, but a tuple (Tact has no tuples)
// https://www.youtube.com/watch?v=451XC5PigRY
}

asm extends fun atInt(self: FakeTuple, index: Int): Int { INDEXVAR }
asm extends fun length(self: FakeTuple): Int { TLEN }
asm fun emptyTuple(): FakeTuple { NIL }
asm extends mutates fun pushInt(self: FakeTuple, value: Int) { TPUSH}

// only for 1-6 elements
extends inline fun median(self: FakeTuple): Int {
let sortedTuple = self.sort();
let DivModResult { quotient: q, remainder: r } = sortedTuple.length().divmod(2);
return (sortedTuple.atInt(q) + sortedTuple.atInt(q - 1 + r)) / 2;
}

// only for 1-6 elements
asm extends fun toTuple(self: Cell): FakeTuple {
<{
CTOS // slice
16 LDU // length slice
s1 s0 XCPU // slice length length
7 LESSINT // slice length f(length < 7)
5 THROWIFNOT // slice length
<{
PLDREF
CTOS
160 LDU
}> PUSHCONT
REPEAT
DROP
DEPTH
TUPLEVAR
}> PUSHCONT
1 1 CALLXARGS
}

// only for 1-6 elements
asm extends fun sort(self: FakeTuple): FakeTuple { // not mutates
CONT:<{
6 EXPLODE
DEC
CONT:<{ }>
CONT:<{ MINMAX }>
CONT:<{
MINMAX // 1 0 2 (0, 2)
-ROT // 2 1 0
MINMAX // 2 0 1 (0, 1)
ROT // 0 1 2
MINMAX // 0 1 2 (1, 2)
}>
CONT:<{
MINMAX // 3 1 0 2 (0, 2)
SWAP2 // 0 2 3 1
MINMAX // 0 2 1 3 (1, 3)
ROT // 0 1 3 2
MINMAX // 0 1 2 3 (2, 3)
SWAP2 // 2 3 0 1
MINMAX // 2 3 0 1 (0, 1)
3 ROLL // 3 0 1 2
MINMAX // 3 0 1 2 (1, 2)
3 ROLL // 0 1 2 3
}>
CONT:<{
MINMAX // 2 4 1 0 3 (0, 3)
SWAP2 // 2 0 3 4 1
MINMAX // 2 0 3 1 4 (1, 4)
s3 s4 XCHG2 // 4 1 3 0 2
MINMAX // 4 1 3 0 2 (0, 2)
SWAP2 // 4 0 2 1 3
MINMAX // 4 0 2 1 3 (1, 3)
s3 XCHG0 // 4 3 2 1 0
MINMAX // 4 3 2 0 1 (0, 1)
s3 s4 s3 XCHG3 // 0 1 3 4 2
MINMAX // 0 1 3 2 4 (2, 4)
s3 XCHG0 // 0 4 3 2 1
MINMAX // 0 4 3 1 2 (1, 2)
SWAP2 // 0 1 2 4 3
MINMAX // 0 1 2 3 4 (3, 4)
-ROT // 0 1 4 2 3
MINMAX // 0 1 4 2 3 (2, 3)
ROT // 0 1 2 3 4
}>
CONT:<{
MINMAX // 4 2 3 1 0 5 (0, 5)
SWAP2 // 4 2 0 5 3 1
MINMAX // 4 2 0 5 1 3 (1, 3)
2 4 BLKSWAP // 0 5 1 3 4 2
MINMAX // 0 5 1 3 2 4 (2, 4)
s3 XCHG0 // 0 5 4 3 2 1
MINMAX // 0 5 4 3 1 2 (1, 2)
SWAP2 // 0 5 1 2 4 3
MINMAX // 0 5 1 2 3 4 (3, 4)
s0 s4 s1 XCHG3 // 0 3 1 4 2 5
MINMAX // 0 3 1 4 2 5 (2, 5)
2 4 BLKSWAP // 1 4 2 5 0 3
MINMAX // 1 4 2 5 0 3 (0, 3)
s5 XCHG0 // 3 4 2 5 0 1
MINMAX // 3 4 2 5 0 1 (0, 1)
s5 s3 s2 XCHG3 // 5 4 0 1 2 3
MINMAX // 5 4 0 1 2 3 (2, 3)
2 4 BLKSWAP // 0 1 2 3 5 4
MINMAX // 0 1 2 3 4 5 (4, 5)
2 3 BLKSWAP // 0 3 4 5 1 2
MINMAX // 0 3 4 5 1 2 (1, 2)
2 3 BLKSWAP // 0 5 1 2 3 4
MINMAX // 0 5 1 2 3 4 (3, 4)
4 ROLL
}>

6 TUPLE

SWAP INDEXVAR EXECUTE DEPTH TUPLEVAR

}> 1 1 CALLXARGS
}
Loading