-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
243 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { | ||
SmartContract, | ||
prop, | ||
ByteString, | ||
method, | ||
assert, | ||
Utils, | ||
Sig, | ||
PubKey, | ||
slice, | ||
byteString2Int, | ||
} from 'scrypt-ts' | ||
import { RabinSig, RabinPubKey, RabinVerifier } from 'scrypt-ts-lib' | ||
|
||
export type Message = { | ||
marker: bigint // 1 byte | ||
timestamp: bigint // 4 bytes LE | ||
price: bigint // 8 bytes LE | ||
decimal: bigint // 1 byte | ||
tradingPair: ByteString | ||
} | ||
|
||
/* | ||
* A betting contract that lets Alice and Bob bet on the price of the BSV-USDC pair | ||
* in the future. The price is obtained from a trusted oracle. | ||
* https://oracle-demo.vercel.app/docs#/v1/V1Controller_getPrice | ||
*/ | ||
export class PriceBet2 extends SmartContract { | ||
// Price target that needs to be reached. | ||
@prop() | ||
targetPrice: bigint | ||
@prop() | ||
decimal: bigint | ||
|
||
// Trading pair, e.g. "BSV-USDC" | ||
@prop() | ||
tradingPair: ByteString | ||
|
||
// Timestamp window in which the price target needs to be reached. | ||
@prop() | ||
timestampFrom: bigint | ||
@prop() | ||
timestampTo: bigint | ||
|
||
// Oracles Rabin public key. | ||
@prop() | ||
oraclePubKey: RabinPubKey | ||
|
||
// Public keys of both players. | ||
@prop() | ||
alicePubKey: PubKey | ||
@prop() | ||
bobPubKey: PubKey | ||
|
||
constructor( | ||
targetPrice: bigint, | ||
decimal: bigint, | ||
tradingPair: ByteString, | ||
timestampFrom: bigint, | ||
timestampTo: bigint, | ||
oraclePubKey: RabinPubKey, | ||
alicePubKey: PubKey, | ||
bobPubKey: PubKey | ||
) { | ||
super(...arguments) | ||
this.targetPrice = targetPrice | ||
this.decimal = decimal | ||
this.tradingPair = tradingPair | ||
this.timestampFrom = timestampFrom | ||
this.timestampTo = timestampTo | ||
this.oraclePubKey = oraclePubKey | ||
this.alicePubKey = alicePubKey | ||
this.bobPubKey = bobPubKey | ||
} | ||
|
||
// Parses signed message from the oracle. | ||
@method() | ||
static parseMessage(msg: ByteString): Message { | ||
return { | ||
marker: Utils.fromLEUnsigned(slice(msg, 0n, 1n)), | ||
timestamp: Utils.fromLEUnsigned(slice(msg, 1n, 5n)), | ||
price: Utils.fromLEUnsigned(slice(msg, 5n, 13n)), | ||
decimal: Utils.fromLEUnsigned(slice(msg, 13n, 14n)), | ||
tradingPair: slice(msg, 14n), | ||
} | ||
} | ||
|
||
@method() | ||
public unlock(msg: ByteString, sig: RabinSig, winnerSig: Sig) { | ||
// Verify oracle signature. | ||
assert( | ||
RabinVerifier.verifySig(msg, sig, this.oraclePubKey), | ||
'Oracle sig verify failed.' | ||
) | ||
|
||
// Decode data. | ||
const message = PriceBet2.parseMessage(msg) | ||
|
||
// Validate data. | ||
assert(message.marker == 2n, 'incorrect oracle message type.') | ||
assert(message.decimal == this.decimal, 'incorrect decimal.') | ||
assert(message.timestamp >= this.timestampFrom, 'Timestamp too early.') | ||
assert(message.timestamp <= this.timestampTo, 'Timestamp too late.') | ||
assert( | ||
message.tradingPair == this.tradingPair, | ||
'incorrect trading pair.' | ||
) | ||
|
||
// Decide winner and check their signature. | ||
const winner = | ||
message.price >= this.targetPrice | ||
? this.alicePubKey | ||
: this.bobPubKey | ||
assert(this.checkSig(winnerSig, winner), 'Winner checkSig failed.') | ||
} | ||
|
||
static parseSig(sig: { s: ByteString; padding: ByteString }): RabinSig { | ||
return { | ||
s: byteString2Int(sig.s + '00'), | ||
padding: sig.padding, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import { PriceBet2 } from '../src/contracts/priceBet2' | ||
import { | ||
ByteString, | ||
bsv, | ||
PubKey, | ||
byteString2Int, | ||
toByteString, | ||
MethodCallOptions, | ||
findSig, | ||
} from 'scrypt-ts' | ||
import { RabinPubKey, RabinSig } from 'scrypt-ts-lib' | ||
import { expect, use } from 'chai' | ||
import { getDefaultSigner } from './utils/helper' | ||
import chaiAsPromised from 'chai-as-promised' | ||
use(chaiAsPromised) | ||
|
||
// All data was pre-fetched from https://oracle-demo.vercel.app/docs | ||
|
||
// https://oracle-demo.vercel.app/info | ||
const PUBKEY = | ||
'496297dbe0759a886eca725ade26baff74ceb23bf6113ce9a2857252ea25afc04fd66bece618c8e96c637f401ce4c21cc2129f62f323f92873ac3fc3f1f15bb921c4bef5c3d30d974208a9b0544cbfb82e73d1639f7329cd81a05c983f62e91673dc3d8e4a567a65f24fb9279ff88b84cf3d52fc2e8a107685dc2382e43a413b066f2bc20a42c15e8f43de1f2b4a6bfc441e6233fe87df8b45bb61238163875684fe0a088822b8597ca2c2e95ad09685206955b8f456bdf18c5f91d38404488571557ab045d03ce551eed96fcef40c0af2e681dfe4583a9121134379fc05f77b32718ac075911ce1fb258720362ada19ff6d92b0d1b8bc95db42076d03fdbac66933b00966cf9c1b5300999647134aaba7460521c3362596083646197d667c1d849f74ae1e0fc8160ff9812e8e12eef886a2662485798c5701f0721408d1c877a0165d1c1e754fcb4e502da16de648348584726bf3831fa4d7129ffe5bc34da51e3c1f1de22e0a8b86b4bfa0709dbea9b30307714a4cb8a28c270c7724f8d25e' | ||
const RESP = { | ||
timestamp: 1700428685, | ||
tradingPair: 'BSV-USDC', | ||
price: 478400, | ||
decimal: 4, | ||
data: '028d7b5a65c04c070000000000044253562d55534443', | ||
signature: { | ||
s: '763e7453f6156fbcf7a063f42f1a73fd21758f0e31d4fee74ae040da73e0f3402abab3bff02ba662132283da5c5c1b7924cd2981d3da72af231a3ef9e9eb971bf3045e419f5f67b0814d1c77a98350b786c80ee6e94000b0e852268e9536e01651562dfee8a1c7988b0e0392fbc48bb64fcce9fa22fee6992fb4938637d063f7406db5b50df8608b696a17fe97fc06bfd2f812c92d4f1cca246d8d61a535f900aa72e7d91ce38c6b0f2746a1816fd8606c15ed0a1f9ced404c19ff4c4b96c5a7b6b1fa7e5e8ba1a82fb7fa1d06f3ed8cfaae3a1f7f55103e95281ba1a45eba7041d6e1fad5960605f8f7481208b5efbd168c9a38f3fdc5d7856a1778b37ad6859118dec36272ba801ac17f9e6ca84e7c30c05e81777d632e046173e629ca5276385f669169fac682d267b21214c8479536f6f8e62c9826e1785f23a0ce976183e289a0b25a4772a93369b3d5828c87b92a6d56e7720693e979dd903f7df749b71538c5703f2fb330887b532e72b1d005a191edc0142b336f1c958c487ef4a152', | ||
padding: '0000000000', | ||
}, | ||
} | ||
|
||
describe('Test SmartContract `PriceBet2`', () => { | ||
let alicePrivKey: bsv.PrivateKey | ||
let bobPrivKey: bsv.PrivateKey | ||
|
||
let priceBet: PriceBet2 | ||
|
||
const decimal = 4 | ||
const currentPrice = Math.round(RESP.price * 10 ** decimal) | ||
|
||
before(() => { | ||
// Prepare inital data. | ||
alicePrivKey = bsv.PrivateKey.fromRandom(bsv.Networks.testnet) | ||
bobPrivKey = bsv.PrivateKey.fromRandom(bsv.Networks.testnet) | ||
const rabinPubKey: bigint = byteString2Int(PUBKEY + '00') | ||
|
||
const targetPriceFloat = 36.3 // USDC | ||
const targetPrice = Math.round(targetPriceFloat * 10 ** decimal) | ||
const timestampFrom = 1700352000n // Sun Nov 19 2023 00:00:00 GMT | ||
const timestampTo = 1700438400n // Mon Nov 20 2023 00:00:00 GMT | ||
const tradingPair = toByteString('BSV-USDC', true) | ||
|
||
PriceBet2.loadArtifact() | ||
priceBet = new PriceBet2( | ||
BigInt(targetPrice), | ||
BigInt(decimal), | ||
tradingPair, | ||
timestampFrom, | ||
timestampTo, | ||
rabinPubKey as RabinPubKey, | ||
PubKey(alicePrivKey.publicKey.toByteString()), | ||
PubKey(bobPrivKey.publicKey.toByteString()) | ||
) | ||
}) | ||
|
||
it('should pass w correct sig and data.', async () => { | ||
// Pick winner. | ||
const winner = | ||
currentPrice >= priceBet.targetPrice ? alicePrivKey : bobPrivKey | ||
const winnerPubKey = winner.publicKey | ||
|
||
// Connect signer. | ||
await priceBet.connect(getDefaultSigner(winner)) | ||
await priceBet.deploy(1) | ||
|
||
const oracleSig: RabinSig = PriceBet2.parseSig(RESP.signature) | ||
|
||
const callContract = async () => | ||
priceBet.methods.unlock( | ||
RESP.data as ByteString, | ||
oracleSig, | ||
(sigResps) => findSig(sigResps, winnerPubKey), | ||
// Method call options: | ||
{ | ||
pubKeyOrAddrToSign: winnerPubKey, | ||
} as MethodCallOptions<PriceBet2> | ||
) | ||
return expect(callContract()).not.rejected | ||
}) | ||
|
||
it('should fail paying wrong player.', async () => { | ||
// Pick loser. | ||
const loser = | ||
currentPrice >= priceBet.targetPrice ? bobPrivKey : alicePrivKey | ||
const loserPubKey = loser.publicKey | ||
|
||
// Connect signer. | ||
await priceBet.connect(getDefaultSigner(loser)) | ||
await priceBet.deploy(1) | ||
|
||
const oracleSig: RabinSig = PriceBet2.parseSig(RESP.signature) | ||
|
||
const callContract = async () => | ||
priceBet.methods.unlock( | ||
RESP.data as ByteString, | ||
oracleSig, | ||
(sigResps) => findSig(sigResps, loserPubKey), | ||
// Method call options: | ||
{ | ||
pubKeyOrAddrToSign: loserPubKey, | ||
} as MethodCallOptions<PriceBet2> | ||
) | ||
|
||
return expect(callContract()).to.be.rejectedWith( | ||
/signature check failed/ | ||
) | ||
}) | ||
}) |