Skip to content

Commit

Permalink
Merge pull request #4 from Datalayer-Storage/server-coin
Browse files Browse the repository at this point in the history
Server coin
  • Loading branch information
MichaelTaylor3D authored Sep 2, 2024
2 parents 56aa598 + 43a87a8 commit d83a6e3
Show file tree
Hide file tree
Showing 11 changed files with 1,210 additions and 491 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ thiserror = "1.0.61"
clvmr = "0.8.0"
tokio = "1.39.3"
chia-wallet-sdk = { version = "0.13.0", features = ["chip-0035"] }
hex-literal = "0.4.1"
num-bigint = "0.4.6"
hex = "0.4.3"

[target.aarch64-unknown-linux-gnu.dependencies]
openssl = { version = "0.10.64", features = ["vendored"] }
Expand Down
110 changes: 63 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
A collection of functions that can be used to interact with datastores on the Chia blockchain.
cd
This library offers the following functions:

- wallet: `selectCoins`, `addFee`, `signCoinSpends`
- drivers: `mintStore`, `adminDelegatedPuzzleFromKey`, `writerDelegatedPuzzleFromKey`, `oracleDelegatedPuzzle`, `oracleSpend`, `updateStoreMetadata`, `updateStoreOwnership`, `meltStore`, `getCost`
- utils: `getCoinId`, `masterPublicKeyToWalletSyntheticKey`, `masterPublicKeyToFirstPuzzleHash`, `masterSecretKeyToWalletSyntheticSecretKey`, `secretKeyToPublicKey`, `puzzleHashToAddress`, `addressToPuzzleHash`, `newLineageProof`, `newEveProof`, `signMessage`, `verifySignedMessage`, `syntheticKeyToPuzzleHash`
- drivers: `mintStore`, `adminDelegatedPuzzleFromKey`, `writerDelegatedPuzzleFromKey`, `oracleDelegatedPuzzle`, `oracleSpend`, `updateStoreMetadata`, `updateStoreOwnership`, `meltStore`, `getCost`, `createServerCoin`, `lookupAndSpendServerCoins`
- utils: `getCoinId`, `masterPublicKeyToWalletSyntheticKey`, `masterPublicKeyToFirstPuzzleHash`, `masterSecretKeyToWalletSyntheticSecretKey`, `secretKeyToPublicKey`, `puzzleHashToAddress`, `addressToPuzzleHash`, `newLineageProof`, `newEveProof`, `signMessage`, `verifySignedMessage`, `syntheticKeyToPuzzleHash`, `morphLauncherId`

The `Peer` class also exposes the following methods: `getAllUnspentCoins`, `syncStore`, `syncStoreFromLauncherId`, `broadcastSpend`, `isCoinSpent`, `getHeaderHash`, `getFeeEstimate`, `getPeak`.
The `Peer` class also exposes the following methods: `getAllUnspentCoins`, `syncStore`, `syncStoreFromLauncherId`, `broadcastSpend`, `isCoinSpent`, `getHeaderHash`, `getFeeEstimate`, `getPeak`, `getHintedCoinStates`, `fetchServerCoin`.

Note that all functions come with detailed JSDoc comments.

Expand Down Expand Up @@ -54,7 +55,7 @@ Where `NETWORK_PREFIX` is `xch` for mainnet and `txch` for testnet.
To 'talk' with the wallet, you will need to initialize a `Peer` object like in the example below:

```js
const peer = await Peer.new('127.0.0.1:58444', 'testnet11', CHIA_CRT, CHIA_KEY)
const peer = await Peer.new("127.0.0.1:58444", "testnet11", CHIA_CRT, CHIA_KEY);
```

The example above connects to a `tesntet11` full node. Note that `CHIA_CRT` is usually `~/.chia/mainnet/config/ssl/wallet/wallet_node.crt` and `CHIA_KEY` is usually `~/.chia/mainnet/config/ssl/wallet/wallet_node.key`. For mainnet, the port is usually `8444`, and the network id is `mainnet`.
Expand All @@ -63,39 +64,41 @@ Making any transaction will require finding available (unspent) coins in the ser

```js
const ph = getServerPuzzleHash();
const coinsResp = await peer.getAllUnspentCoins(ph, MIN_HEIGHT, MIN_HEIGHT_HEADER_HASH);
const coinsResp = await peer.getAllUnspentCoins(
ph,
MIN_HEIGHT,
MIN_HEIGHT_HEADER_HASH
);
const coins = selectCoins(coinsResp.coins, feeBigInt + BigInt(1));
```

You can speed up coin lookup by setting `MIN_HEIGHT` and `MIN_HEIGHT_HEADER_HASH` to point to a block just before wallet creation (or before the first fund tx was confirmed). Alternatively, you can set them to `null` and the network's genesis challenge. When selecting coins, make sure to include the fee in the total amount.

The next step is to generate coin spends using drivers:

```js
const serverKey = getPublicSyntheticKey();
const successResponse = await mintStore(
getPublicSyntheticKey(),
coins,
rootHash,
label,
description,
sizeBigInt,
ownerPuzzleHash,
[
adminDelegatedPuzzleFromKey(serverKey),
writerDelegatedPuzzleFromKey(serverKey),
oracleDelegatedPuzzle(ownerPuzzleHash, oracleFeeBigInt)
],
feeBigInt
);
```

The code above is used to mint stores. Note that a success response not only contains unsigned coin spends, but also returns a new `DataStore` object that can be used to sync or spend the store in the future. Note that some drivers will not require coins, only the information of the store being spent:

```js
const resp = meltStore(
parseDataStore(info),
ownerPublicKey
);
getPublicSyntheticKey(),
coins,
rootHash,
label,
description,
sizeBigInt,
ownerPuzzleHash,
[
adminDelegatedPuzzleFromKey(serverKey),
writerDelegatedPuzzleFromKey(serverKey),
oracleDelegatedPuzzle(ownerPuzzleHash, oracleFeeBigInt),
],
feeBigInt
);
```

The code above is used to mint stores. Note that a success response not only contains unsigned coin spends, but also returns a new `DataStore` object that can be used to sync or spend the store in the future. Note that some drivers will not require coins, only the information of the store being spent:

```js
const resp = meltStore(parseDataStore(info), ownerPublicKey);
```

In that case, the 'basic' transaction only spends the store - to add fees, you'll need to call `addFee` and make sure to include the returned coin spends in the final bundle:
Expand All @@ -115,21 +118,23 @@ Note that `true` indicates that the transaction is being signed for testnet11, w
Broadcasting a bundle is as easy as:

```js
const err = await peer.broadcastSpend(
coinSpends,
[sig]
);
const err = await peer.broadcastSpend(coinSpends, [sig]);
```

To confirm the transaction, you can just confirm that the datastore coin was spent on-chain:

```js
const confirmed = await peer.isCoinSpent(getCoinId(info.coin), MIN_HEIGHT, MIN_HEIGHT_HEADER_HASH);
const confirmed = await peer.isCoinSpent(
getCoinId(info.coin),
MIN_HEIGHT,
MIN_HEIGHT_HEADER_HASH
);
```

## More Examples

### Transferring the Store to a New Owner

Suppose `info` is holding the current store's information and you want to transfer it to `newOwnerPuzzleHash`. You can do this as follows:

```js
Expand All @@ -140,7 +145,7 @@ const {coinSpends, newInfo} = updateStoreOwnership(
currentOwnerPublicKey,
null,
);

/* optionally add a fee via 'addFee' - you'll also need to get 'server_sig' via 'signCoinSpends' */

const sig = /* fetch from user */;
Expand All @@ -153,7 +158,7 @@ const err = await peer.broadcastSpend(
[sig /* add 'server_sig' if adding fee */ ]
);
// check that err === "" <-> successful mempool inclusion

/* wait for tx to be confirmed */
var confirmed = await peer.isCoinSpent(getCoinId(info.coin), MIN_HEIGHT, MIN_HEIGHT_HEADER_HASH);
while(!confirmed) {
Expand All @@ -165,40 +170,51 @@ Note that, when changing ownership, either the owner's or an admin's synthetic k

Waiting for transactions is usually more complicated than the snippet above - mempool items are sometimes kicked out when transactions with higher fees can fill the mempool, meaning that the `while` loop would run infinitely.

### Syncing a Store & Verifying Ownership
### Syncing a Store & Verifying Ownership

To sync a store, you'll first need a peer. Recall that we've previously initialized a peer as:

```js
const CHIA_CRT = path.join(os.homedir(), '.chia/mainnet/config/ssl/wallet/wallet_node.crt');
const CHIA_KEY = path.join(os.homedir(), '.chia/mainnet/config/ssl/wallet/wallet_node.key');
const CHIA_CRT = path.join(
os.homedir(),
".chia/mainnet/config/ssl/wallet/wallet_node.crt"
);
const CHIA_KEY = path.join(
os.homedir(),
".chia/mainnet/config/ssl/wallet/wallet_node.key"
);
// ...
const peer = await Peer.new('127.0.0.1:58444', 'testnet11', CHIA_CRT, CHIA_KEY)
const peer = await Peer.new("127.0.0.1:58444", "testnet11", CHIA_CRT, CHIA_KEY);
```

To sync, you'll also need two other values, `MIN_HEIGHT` and `MIN_HEIGHT_HEADER_HASH`. These variables represent information relating to the block you want to start syncing from - higher heights lead to faster sync times. If you wish to sync from genesis, use a height of `null` and a header hash equal to the network's genesis challenge.

Syncing a store using its launcher id is as easy as:

```js
const {
latestInfo, latestHeight
} = await peer.syncStoreFromLauncherId(launcherId, MIN_HEIGHT, MIN_HEIGHT_HEADER_HASH, false);
const { latestInfo, latestHeight } = await peer.syncStoreFromLauncherId(
launcherId,
MIN_HEIGHT,
MIN_HEIGHT_HEADER_HASH,
false
);
```

If you already have a `DataStore` object, you can use it to 'bootstrap' the syncing process and minimize the time it takes to fetch the latest info:


```js
const {
latestInfo, latestHeight
} = await peer.syncStore(oldStoreInfo, MIN_HEIGHT, MIN_HEIGHT_HEADER_HASH, false);
const { latestInfo, latestHeight } = await peer.syncStore(
oldStoreInfo,
MIN_HEIGHT,
MIN_HEIGHT_HEADER_HASH,
false
);
```

With the latest store info in the `latestInfo` variable, checking that the current store owner is `myPuzzleHash` can be done as follows:

```js
if(latestInfo.ownerPuzzleHash === myPuzzleHash) {
if (latestInfo.ownerPuzzleHash === myPuzzleHash) {
doSomething();
}
```
Expand Down
68 changes: 68 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ export interface Coin {
puzzleHash: Buffer
amount: bigint
}
/**
* Represents a full coin state on the Chia blockchain.
*
* @property {Coin} coin - The coin.
* @property {Buffer} spentHeight - The height the coin was spent at, if it was spent.
* @property {Buffer} createdHeight - The height the coin was created at.
*/
export interface CoinState {
coin: Coin
spentHeight?: bigint
createdHeight?: bigint
}
/**
* Represents a coin spend on the Chia blockchain.
*
Expand Down Expand Up @@ -59,6 +71,18 @@ export interface Proof {
lineageProof?: LineageProof
eveProof?: EveProof
}
/**
* Represents a mirror coin with a potentially morphed launcher id.
*
* @property {Coin} coin - The coin.
* @property {Buffer} p2PuzzleHash - The puzzle hash that owns the server coin.
* @property {Array<string>} memoUrls - The memo URLs that serve the data store being mirrored.
*/
export interface ServerCoin {
coin: Coin
p2PuzzleHash: Buffer
memoUrls: Array<string>
}
/**
* Creates a new lineage proof.
*
Expand Down Expand Up @@ -162,6 +186,34 @@ export interface UnspentCoinsResponse {
* @returns {Vec<Coin>} Array of selected coins.
*/
export declare function selectCoins(allCoins: Array<Coin>, totalAmount: bigint): Array<Coin>
/**
* Adds an offset to a launcher id to make it deterministically unique from the original.
*
* @param {Buffer} launcherId - The original launcher id.
* @param {BigInt} offset - The offset to add.
*/
export declare function morphLauncherId(launcherId: Buffer, offset: bigint): Buffer
/**
* Creates a new mirror coin with the given URLs.
*
* @param {Buffer} syntheticKey - The synthetic key used by the wallet.
* @param {Vec<Coin>} selectedCoins - Coins to be used for minting, as retured by `select_coins`. Note that, besides the fee, 1 mojo will be used to create the mirror coin.
* @param {Buffer} hint - The hint for the mirror coin, usually the original or morphed launcher id.
* @param {Vec<String>} uris - The URIs of the mirrors.
* @param {BigInt} amount - The amount to use for the created coin.
* @param {BigInt} fee - The fee to use for the transaction.
*/
export declare function createServerCoin(syntheticKey: Buffer, selectedCoins: Array<Coin>, hint: Buffer, uris: Array<string>, amount: bigint, fee: bigint): Array<CoinSpend>
/**
* Spends the mirror coins to make them unusable in the future.
*
* @param {Peer} peer - The peer connection to the Chia node.
* @param {Buffer} syntheticKey - The synthetic key used by the wallet.
* @param {Vec<Coin>} selectedCoins - Coins to be used for minting, as retured by `select_coins`. Note that the server coins will count towards the fee.
* @param {BigInt} fee - The fee to use for the transaction.
* @param {bool} forTestnet - True for testnet, false for mainnet.
*/
export declare function lookupAndSpendServerCoins(peer: Peer, syntheticKey: Buffer, selectedCoins: Array<Coin>, fee: bigint, forTestnet: boolean): Promise<Array<CoinSpend>>
/**
* Mints a new datastore.
*
Expand Down Expand Up @@ -361,6 +413,22 @@ export declare class Peer {
* @returns {Promise<UnspentCoinsResponse>} The unspent coins response.
*/
getAllUnspentCoins(puzzleHash: Buffer, previousHeight: number | undefined | null, previousHeaderHash: Buffer): Promise<UnspentCoinsResponse>
/**
* Retrieves all hinted coin states that are unspent on the chain. Note that coins part of spend bundles that are pending in the mempool will also be included.
*
* @param {Buffer} puzzleHash - Puzzle hash to lookup hinted coins for.
* @param {bool} forTestnet - True for testnet, false for mainnet.
* @returns {Promise<Vec<Coin>>} The unspent coins response.
*/
getHintedCoinStates(puzzleHash: Buffer, forTestnet: boolean): Promise<Array<CoinState>>
/**
* Fetches the server coin from a given coin state.
*
* @param {CoinState} coinState - The coin state.
* @param {BigInt} maxCost - The maximum cost to use when parsing the coin. For example, `11_000_000_000`.
* @returns {Promise<ServerCoin>} The server coin.
*/
fetchServerCoin(coinState: CoinState, maxCost: bigint): Promise<ServerCoin>
/**
* Synchronizes a datastore.
*
Expand Down
5 changes: 4 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,12 +310,15 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}

const { newLineageProof, newEveProof, Peer, selectCoins, mintStore, oracleSpend, addFee, masterPublicKeyToWalletSyntheticKey, masterPublicKeyToFirstPuzzleHash, masterSecretKeyToWalletSyntheticSecretKey, secretKeyToPublicKey, puzzleHashToAddress, addressToPuzzleHash, adminDelegatedPuzzleFromKey, writerDelegatedPuzzleFromKey, oracleDelegatedPuzzle, signCoinSpends, getCoinId, updateStoreMetadata, updateStoreOwnership, meltStore, signMessage, verifySignedMessage, syntheticKeyToPuzzleHash, getCost } = nativeBinding
const { newLineageProof, newEveProof, Peer, selectCoins, morphLauncherId, createServerCoin, lookupAndSpendServerCoins, mintStore, oracleSpend, addFee, masterPublicKeyToWalletSyntheticKey, masterPublicKeyToFirstPuzzleHash, masterSecretKeyToWalletSyntheticSecretKey, secretKeyToPublicKey, puzzleHashToAddress, addressToPuzzleHash, adminDelegatedPuzzleFromKey, writerDelegatedPuzzleFromKey, oracleDelegatedPuzzle, signCoinSpends, getCoinId, updateStoreMetadata, updateStoreOwnership, meltStore, signMessage, verifySignedMessage, syntheticKeyToPuzzleHash, getCost } = nativeBinding

module.exports.newLineageProof = newLineageProof
module.exports.newEveProof = newEveProof
module.exports.Peer = Peer
module.exports.selectCoins = selectCoins
module.exports.morphLauncherId = morphLauncherId
module.exports.createServerCoin = createServerCoin
module.exports.lookupAndSpendServerCoins = lookupAndSpendServerCoins
module.exports.mintStore = mintStore
module.exports.oracleSpend = oracleSpend
module.exports.addFee = addFee
Expand Down
Loading

0 comments on commit d83a6e3

Please sign in to comment.