├── .nvmrc ├── .gitignore ├── tsconfig.build.json ├── docs └── protocol.png ├── src ├── schema │ ├── index.ts │ ├── teiki │ │ ├── common.ts │ │ ├── tags.ts │ │ ├── backing.ts │ │ ├── kolours.ts │ │ ├── treasury.ts │ │ ├── meta-protocol.ts │ │ ├── project.ts │ │ └── protocol.ts │ └── helios.ts ├── transactions │ ├── kolours │ │ └── constants.ts │ ├── meta-protocol │ │ ├── burn-teiki.ts │ │ ├── evolve-teiki.ts │ │ ├── cancel.ts │ │ ├── apply.ts │ │ ├── propose.ts │ │ └── bootstrap.ts │ ├── protocol │ │ ├── reclaim-scripts.ts │ │ ├── cancel.ts │ │ ├── withdraw.ts │ │ ├── propose.ts │ │ ├── apply.ts │ │ └── bootstrap.ts │ ├── constants.ts │ ├── deploy-scripts.ts │ ├── fraction.ts │ ├── meta-data.ts │ ├── project │ │ ├── update-staking-delegation-management.ts │ │ ├── delegate.ts │ │ ├── initiate-close.ts │ │ ├── initiate-delist.ts │ │ ├── allocate-staking.ts │ │ └── finalize-delist.ts │ ├── treasury │ │ ├── dedicated-treasury │ │ │ ├── revoke.ts │ │ │ └── withdraw-ada.ts │ │ └── open-treasury │ │ │ └── withdraw-ada.ts │ └── backing │ │ └── claim-rewards-by-flower.ts ├── contracts │ ├── protocol │ │ ├── protocol.nft │ │ │ ├── types.ts │ │ │ └── main.ts │ │ ├── protocol-proposal.v │ │ │ ├── types.ts │ │ │ └── main.ts │ │ ├── protocol-script.v │ │ │ └── main.ts │ │ ├── protocol-params.v │ │ │ ├── types.ts │ │ │ └── main.ts │ │ └── protocol.sv │ │ │ └── main.ts │ ├── meta-protocol │ │ ├── teiki-plant.nft │ │ │ ├── types.ts │ │ │ └── main.ts │ │ ├── teiki.mp │ │ │ ├── types.ts │ │ │ └── main.ts │ │ └── teiki-plant.v │ │ │ ├── types.ts │ │ │ └── main.ts │ ├── project │ │ ├── project.at │ │ │ └── types.ts │ │ ├── project-script.v │ │ │ └── types.ts │ │ ├── project-detail.v │ │ │ └── types.ts │ │ ├── project.v │ │ │ └── types.ts │ │ └── project.sv │ │ │ └── main.ts │ ├── backing │ │ ├── backing.v │ │ │ ├── types.ts │ │ │ └── main.ts │ │ └── proof-of-backing.mp │ │ │ └── types.ts │ ├── treasury │ │ ├── open-treasury.v │ │ │ └── types.ts │ │ ├── dedicated-treasury.v │ │ │ └── types.ts │ │ └── shared-treasury.v │ │ │ └── types.ts │ ├── sample-migration │ │ └── sample-migrate-token.mp │ │ │ └── main.ts │ ├── common │ │ ├── types.ts │ │ ├── fraction.ts │ │ ├── constants.ts │ │ └── helpers.ts │ ├── kolours │ │ └── kolour.nft │ │ │ └── main.ts │ ├── program.ts │ └── compile.ts ├── commands │ ├── compile-kolour-scripts.ts │ ├── utils.ts │ └── compile-scripts.ts ├── utils.ts ├── cli │ ├── meta-protocol │ │ ├── apply.ts │ │ └── propose.ts │ ├── bootstrap-kolour-nft.ts │ ├── protocol │ │ ├── apply.ts │ │ └── propose.ts │ └── utils.ts ├── types.ts ├── helpers │ ├── helios.ts │ ├── lucid.ts │ └── schema.ts └── json.ts ├── tsconfig.test.json ├── tests ├── setup.ts ├── compile-scripts │ ├── kolours │ │ └── kolour.nft.test.ts │ ├── project │ │ ├── project.at.test.ts │ │ ├── project.v.test.ts │ │ ├── project-detail.v.test.ts │ │ ├── project-script.v.test.ts │ │ └── project.sv.test.ts │ ├── meta-protocol │ │ ├── teiki.mp.test.ts │ │ ├── teiki-plant.v.test.ts │ │ └── teiki-plant.nft.test.ts │ ├── protocol │ │ ├── protocol.sv.test.ts │ │ ├── protocol-params.v.test.ts │ │ ├── protocol-script.v.test.ts │ │ ├── protocol.nft.test.ts │ │ └── protocol-proposal.v.test.ts │ ├── treasury │ │ ├── open-treasury.v.test.ts │ │ ├── dedicated-treasury.v.test.ts │ │ └── shared-treasury.v.test.ts │ ├── backing │ │ ├── backing.v.test.ts │ │ └── proof-of-backing.mp.test.ts │ └── base.ts ├── utils.ts ├── emulator.ts ├── schema.test.ts ├── kolour-txs.test.ts └── treasury │ └── open-treasury-txs.test.ts ├── .github └── workflows │ └── main.yml ├── loader.js ├── tsconfig.json ├── jest.config.ts ├── README.md ├── .eslintrc.yml └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .env 4 | .DS_Store 5 | .envrc 6 | .*.local 7 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /docs/protocol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kreate-art/kreate-protocol/HEAD/docs/protocol.png -------------------------------------------------------------------------------- /src/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./uplc"; 2 | export * from "./helios"; 3 | export * from "./serialization"; 4 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/transactions/kolours/constants.ts: -------------------------------------------------------------------------------- 1 | export const KOLOUR_TX_MAX_DURATION = 600_000n; // 10 minutes 2 | export const KOLOUR_TX_DEADLINE = 2_208_988_799_999n; // 2039/12/31 23:59:59 GMT 3 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | import { setDefaultOptions } from "@/contracts/compile"; 2 | 3 | setDefaultOptions({ 4 | simplify: Boolean(Number(process.env.CONTRACTS_SIMPLIFY || "0")), 5 | }); 6 | -------------------------------------------------------------------------------- /src/contracts/protocol/protocol.nft/types.ts: -------------------------------------------------------------------------------- 1 | import { header, helios } from "../../program"; 2 | 3 | export default helios` 4 | ${header("module", "nft__protocol__types")} 5 | 6 | enum Redeemer { 7 | Bootstrap 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/contracts/meta-protocol/teiki-plant.nft/types.ts: -------------------------------------------------------------------------------- 1 | import { header, helios } from "../../program"; 2 | 3 | export default helios` 4 | ${header("module", "nft__teiki_plant__types")} 5 | 6 | enum Redeemer { 7 | Bootstrap 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/contracts/meta-protocol/teiki.mp/types.ts: -------------------------------------------------------------------------------- 1 | import { header, helios } from "../../program"; 2 | 3 | export default helios` 4 | ${header("module", "mp__teiki__types")} 5 | 6 | enum Redeemer { 7 | Mint 8 | Burn 9 | Evolve 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /src/schema/teiki/common.ts: -------------------------------------------------------------------------------- 1 | import { ByteArray, Static, String, Struct } from "../uplc"; 2 | 3 | export const ProjectId = Struct({ id: ByteArray }); 4 | export type ProjectId = Static; 5 | 6 | export const IpfsCid = Struct({ cid: String }); 7 | export type IpfsCid = Static; 8 | -------------------------------------------------------------------------------- /tests/compile-scripts/kolours/kolour.nft.test.ts: -------------------------------------------------------------------------------- 1 | import getKolourNft from "@/contracts/kolours/kolour.nft/main"; 2 | 3 | import { compileAndLog } from "../base"; 4 | 5 | test("compile: NFT | Kolour", () => { 6 | const size = compileAndLog(getKolourNft({ producerPkh: "" })); 7 | expect(size).toBeGreaterThan(0); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/compile-scripts/project/project.at.test.ts: -------------------------------------------------------------------------------- 1 | import getProjectAt from "@/contracts/project/project.at/main"; 2 | 3 | import { compileAndLog } from "../base"; 4 | 5 | test("compile: AT | Project", () => { 6 | const size = compileAndLog(getProjectAt({ protocolNftMph: "" })); 7 | expect(size).toBeGreaterThan(0); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/compile-scripts/meta-protocol/teiki.mp.test.ts: -------------------------------------------------------------------------------- 1 | import getTeikiMp from "@/contracts/meta-protocol/teiki.mp/main"; 2 | 3 | import { compileAndLog } from "../base"; 4 | 5 | test("compile: MP | Teiki", () => { 6 | const size = compileAndLog(getTeikiMp({ teikiPlantNftMph: "" })); 7 | expect(size).toBeGreaterThan(0); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/compile-scripts/protocol/protocol.sv.test.ts: -------------------------------------------------------------------------------- 1 | import getProtocolSv from "@/contracts/protocol/protocol.sv/main"; 2 | 3 | import { compileAndLog } from "../base"; 4 | 5 | test("compile: SV | Protocol", () => { 6 | const size = compileAndLog(getProtocolSv({ protocolNftMph: "" })); 7 | expect(size).toBeGreaterThan(0); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/compile-scripts/treasury/open-treasury.v.test.ts: -------------------------------------------------------------------------------- 1 | import getOpenTreasuryV from "@/contracts/treasury/open-treasury.v/main"; 2 | 3 | import { compileAndLog } from "../base"; 4 | 5 | test("compile: V | Open Treasury", () => { 6 | const size = compileAndLog(getOpenTreasuryV({ protocolNftMph: "" })); 7 | expect(size).toBeGreaterThan(0); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/compile-scripts/meta-protocol/teiki-plant.v.test.ts: -------------------------------------------------------------------------------- 1 | import getTeikiPlantV from "@/contracts/meta-protocol/teiki-plant.v/main"; 2 | 3 | import { compileAndLog } from "../base"; 4 | 5 | test("compile: V | Teiki Plant", () => { 6 | const size = compileAndLog(getTeikiPlantV({ teikiPlantNftMph: "" })); 7 | expect(size).toBeGreaterThan(0); 8 | }); 9 | -------------------------------------------------------------------------------- /src/contracts/project/project.at/types.ts: -------------------------------------------------------------------------------- 1 | import { header, helios } from "../../program"; 2 | 3 | export default helios` 4 | ${header("module", "at__project__types")} 5 | 6 | enum Redeemer { 7 | NewProject { project_seed: TxOutputId } 8 | AllocateStaking 9 | DeallocateStaking 10 | MigrateOut 11 | MigrateIn 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /tests/compile-scripts/protocol/protocol-params.v.test.ts: -------------------------------------------------------------------------------- 1 | import getProtocolParamsV from "@/contracts/protocol/protocol-params.v/main"; 2 | 3 | import { compileAndLog } from "../base"; 4 | 5 | test("compile: V | Protocol Params", () => { 6 | const size = compileAndLog(getProtocolParamsV({ protocolNftMph: "" })); 7 | expect(size).toBeGreaterThan(0); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/compile-scripts/protocol/protocol-script.v.test.ts: -------------------------------------------------------------------------------- 1 | import getProtocolScriptV from "@/contracts/protocol/protocol-script.v/main"; 2 | 3 | import { compileAndLog } from "../base"; 4 | 5 | test("compile: V | Protocol Script", () => { 6 | const size = compileAndLog(getProtocolScriptV({ protocolNftMph: "" })); 7 | expect(size).toBeGreaterThan(0); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/compile-scripts/protocol/protocol.nft.test.ts: -------------------------------------------------------------------------------- 1 | import getProtocolNft from "@/contracts/protocol/protocol.nft/main"; 2 | 3 | import { BLANK_OUT_REF, compileAndLog } from "../base"; 4 | 5 | test("compile: NFT | Protocol", () => { 6 | const size = compileAndLog(getProtocolNft({ protocolSeed: BLANK_OUT_REF })); 7 | expect(size).toBeGreaterThan(0); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/compile-scripts/backing/backing.v.test.ts: -------------------------------------------------------------------------------- 1 | import getBackingV from "@/contracts/backing/backing.v/main"; 2 | 3 | import { compileAndLog } from "../base"; 4 | 5 | test("compile: V | Backing", () => { 6 | const size = compileAndLog( 7 | getBackingV({ proofOfBackingMph: "", protocolNftMph: "" }) 8 | ); 9 | expect(size).toBeGreaterThan(0); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/compile-scripts/protocol/protocol-proposal.v.test.ts: -------------------------------------------------------------------------------- 1 | import getProtocolProposalV from "@/contracts/protocol/protocol-proposal.v/main"; 2 | 3 | import { compileAndLog } from "../base"; 4 | 5 | test("compile: V | Protocol Proposal", () => { 6 | const size = compileAndLog(getProtocolProposalV({ protocolNftMph: "" })); 7 | expect(size).toBeGreaterThan(0); 8 | }); 9 | -------------------------------------------------------------------------------- /src/contracts/project/project-script.v/types.ts: -------------------------------------------------------------------------------- 1 | import { header, helios } from "../../program"; 2 | 3 | export default helios` 4 | ${header("module", "v__project_script__types")} 5 | 6 | struct Datum { 7 | project_id: ByteArray 8 | stake_key_deposit: Int 9 | } 10 | 11 | enum Redeemer { 12 | Close 13 | Delist 14 | Migrate 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /src/commands/compile-kolour-scripts.ts: -------------------------------------------------------------------------------- 1 | import { UplcProgram } from "@hyperionbt/helios"; 2 | 3 | import { compile } from "@/contracts/compile"; 4 | import getKolourNft, { 5 | Params as KolourNftParams, 6 | } from "@/contracts/kolours/kolour.nft/main"; 7 | 8 | export function compileKolourNftScript(params: KolourNftParams): UplcProgram { 9 | return compile(getKolourNft(params)); 10 | } 11 | -------------------------------------------------------------------------------- /tests/compile-scripts/project/project.v.test.ts: -------------------------------------------------------------------------------- 1 | import getProjectV from "@/contracts/project/project.v/main"; 2 | 3 | import { compileAndLog } from "../base"; 4 | 5 | test("compile: V | Project", () => { 6 | const size = compileAndLog( 7 | getProjectV({ 8 | projectAtMph: "", 9 | protocolNftMph: "", 10 | }) 11 | ); 12 | expect(size).toBeGreaterThan(0); 13 | }); 14 | -------------------------------------------------------------------------------- /src/contracts/backing/backing.v/types.ts: -------------------------------------------------------------------------------- 1 | import { header, helios } from "../../program"; 2 | 3 | export default helios` 4 | ${header("module", "v__backing__types")} 5 | 6 | struct Datum { 7 | project_id: ByteArray 8 | backer_address: Address 9 | backed_at: Time 10 | milestone_backed: Int 11 | } 12 | 13 | enum Redeemer { 14 | Unback 15 | Migrate 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /tests/compile-scripts/meta-protocol/teiki-plant.nft.test.ts: -------------------------------------------------------------------------------- 1 | import getTeikiPlantNft from "@/contracts/meta-protocol/teiki-plant.nft/main"; 2 | 3 | import { BLANK_OUT_REF, compileAndLog } from "../base"; 4 | 5 | test("compile: NFT | Teiki Plant", () => { 6 | const size = compileAndLog( 7 | getTeikiPlantNft({ teikiPlantSeed: BLANK_OUT_REF }) 8 | ); 9 | expect(size).toBeGreaterThan(0); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/compile-scripts/project/project-detail.v.test.ts: -------------------------------------------------------------------------------- 1 | import getProjectDetailV from "@/contracts/project/project-detail.v/main"; 2 | 3 | import { compileAndLog } from "../base"; 4 | 5 | test("compile: V | Project Detail", () => { 6 | const size = compileAndLog( 7 | getProjectDetailV({ 8 | projectAtMph: "", 9 | protocolNftMph: "", 10 | }) 11 | ); 12 | expect(size).toBeGreaterThan(0); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/compile-scripts/project/project-script.v.test.ts: -------------------------------------------------------------------------------- 1 | import getProjectScriptV from "@/contracts/project/project-script.v/main"; 2 | 3 | import { compileAndLog } from "../base"; 4 | 5 | test("compile: V | Project Script", () => { 6 | const size = compileAndLog( 7 | getProjectScriptV({ 8 | projectAtMph: "", 9 | protocolNftMph: "", 10 | }) 11 | ); 12 | expect(size).toBeGreaterThan(0); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/compile-scripts/treasury/dedicated-treasury.v.test.ts: -------------------------------------------------------------------------------- 1 | import getDedicatedTreasuryV from "@/contracts/treasury/dedicated-treasury.v/main"; 2 | 3 | import { compileAndLog } from "../base"; 4 | 5 | test("compile: V | Dedicated Treasury", () => { 6 | const size = compileAndLog( 7 | getDedicatedTreasuryV({ 8 | projectAtMph: "", 9 | protocolNftMph: "", 10 | }) 11 | ); 12 | expect(size).toBeGreaterThan(0); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/compile-scripts/treasury/shared-treasury.v.test.ts: -------------------------------------------------------------------------------- 1 | import getSharedTreasuryV from "@/contracts/treasury/shared-treasury.v/main"; 2 | 3 | import { compileAndLog } from "../base"; 4 | 5 | test("compile: V | Shared Treasury", () => { 6 | const size = compileAndLog( 7 | getSharedTreasuryV({ 8 | projectAtMph: "", 9 | protocolNftMph: "", 10 | teikiMph: "", 11 | }) 12 | ); 13 | expect(size).toBeGreaterThan(0); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/compile-scripts/backing/proof-of-backing.mp.test.ts: -------------------------------------------------------------------------------- 1 | import getProofOfBackingMp from "@/contracts/backing/proof-of-backing.mp/main"; 2 | 3 | import { compileAndLog } from "../base"; 4 | 5 | test("compile: MP | Proof of Backing", () => { 6 | const size = compileAndLog( 7 | getProofOfBackingMp({ 8 | projectAtMph: "", 9 | protocolNftMph: "", 10 | teikiMph: "", 11 | }) 12 | ); 13 | expect(size).toBeGreaterThan(0); 14 | }); 15 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function assert( 2 | condition: unknown, 3 | message?: string 4 | ): asserts condition { 5 | if (!condition) throw new Error(message || "assertion failed"); 6 | } 7 | 8 | export function nullIfFalsy(item: T | null | undefined): T | null { 9 | return item ? item : null; 10 | } 11 | 12 | // Truncate to the beginning of a second, due to how ouroboros works. 13 | export function trimToSlot(time: number) { 14 | return time - (time % 1000); 15 | } 16 | -------------------------------------------------------------------------------- /tests/compile-scripts/base.ts: -------------------------------------------------------------------------------- 1 | import { bytesToHex } from "@hyperionbt/helios"; 2 | 3 | import { compile } from "@/contracts/compile"; 4 | import { HeliosScript } from "@/contracts/program"; 5 | 6 | export function compileAndLog(script: HeliosScript) { 7 | const uplcProgram = compile(script); 8 | const size = uplcProgram.calcSize(); 9 | console.log(`${size} | ${bytesToHex(uplcProgram.hash())}`); 10 | return size; 11 | } 12 | 13 | export const BLANK_OUT_REF = { txHash: "00".repeat(32), outputIndex: 0 }; 14 | -------------------------------------------------------------------------------- /src/contracts/treasury/open-treasury.v/types.ts: -------------------------------------------------------------------------------- 1 | import { header, helios, module } from "../../program"; 2 | 3 | export default helios` 4 | ${header("module", "v__open_treasury__types")} 5 | 6 | import { TreasuryTag } 7 | from ${module("common__types")} 8 | 9 | struct Datum { 10 | governor_ada: Int 11 | tag: TreasuryTag 12 | } 13 | 14 | enum Redeemer { 15 | CollectDelayedStakingRewards { 16 | staking_withdrawals: Map[StakingValidatorHash]Int 17 | } 18 | WithdrawAda 19 | Migrate 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /src/contracts/treasury/dedicated-treasury.v/types.ts: -------------------------------------------------------------------------------- 1 | import { header, helios, module } from "../../program"; 2 | 3 | export default helios` 4 | ${header("module", "v__dedicated_treasury__types")} 5 | 6 | import { TreasuryTag } 7 | from ${module("common__types")} 8 | 9 | struct Datum { 10 | project_id: ByteArray 11 | governor_ada: Int 12 | tag: TreasuryTag 13 | } 14 | 15 | enum Redeemer { 16 | CollectFees { 17 | fees: Int 18 | split: Bool 19 | } 20 | WithdrawAda 21 | Revoke 22 | Migrate 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /src/contracts/protocol/protocol-proposal.v/types.ts: -------------------------------------------------------------------------------- 1 | import { header, helios, module } from "../../program"; 2 | 3 | export default helios` 4 | ${header("module", "v__protocol_proposal__types")} 5 | 6 | import { Datum as PParamsDatum } 7 | from ${module("v__protocol_params__types")} 8 | 9 | struct Proposal { 10 | in_effect_at: Time 11 | base: TxOutputId 12 | params: PParamsDatum 13 | } 14 | 15 | struct Datum { 16 | proposal: Option[Proposal] 17 | } 18 | 19 | enum Redeemer { 20 | Propose 21 | Apply 22 | Cancel 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | ci: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repository 16 | uses: actions/checkout@v3 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version-file: '.nvmrc' 21 | cache: npm 22 | - run: npm ci 23 | - run: npm run typecheck 24 | - run: npm run lint 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /src/contracts/project/project-detail.v/types.ts: -------------------------------------------------------------------------------- 1 | import { header, helios } from "../../program"; 2 | 3 | export default helios` 4 | ${header("module", "v__project_detail__types")} 5 | 6 | enum Redeemer { 7 | WithdrawFunds 8 | Update 9 | Close 10 | Delist 11 | Migrate 12 | } 13 | 14 | struct Sponsorship { 15 | amount: Int 16 | until: Time 17 | } 18 | 19 | struct Datum { 20 | project_id: ByteArray 21 | withdrawn_funds: Int 22 | sponsorship: Option[Sponsorship] 23 | information_cid: String 24 | last_announcement_cid: Option[String] 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /src/contracts/backing/proof-of-backing.mp/types.ts: -------------------------------------------------------------------------------- 1 | import { header, helios } from "../../program"; 2 | 3 | export default helios` 4 | ${header("module", "mp__proof_of_backing__types")} 5 | 6 | struct Plant { 7 | is_matured: Bool 8 | backing_output_id: TxOutputId 9 | backing_amount: Int 10 | unbacked_at: Time 11 | project_id: ByteArray 12 | backer_address: Address 13 | backed_at: Time 14 | milestone_backed: Int 15 | } 16 | 17 | enum Redeemer { 18 | Plant { cleanup: Bool } 19 | ClaimRewards { flowers: []Plant } 20 | MigrateOut 21 | MigrateIn 22 | Burn 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /loader.js: -------------------------------------------------------------------------------- 1 | // Temporary fix 2 | // https://github.com/TypeStrong/ts-node/discussions/1450#discussioncomment-1806115 3 | import { pathToFileURL } from "url"; 4 | 5 | import { resolve as resolveTs } from "ts-node/esm"; 6 | import * as tsConfigPaths from "tsconfig-paths"; 7 | 8 | const { absoluteBaseUrl, paths } = tsConfigPaths.loadConfig(); 9 | const matchPath = tsConfigPaths.createMatchPath(absoluteBaseUrl, paths); 10 | 11 | export function resolve(specifier, ctx, defaultResolve) { 12 | const match = matchPath(specifier); 13 | return match 14 | ? resolveTs(pathToFileURL(`${match}`).href, ctx, defaultResolve) 15 | : resolveTs(specifier, ctx, defaultResolve); 16 | } 17 | 18 | export { load, transformSource } from "ts-node/esm"; 19 | -------------------------------------------------------------------------------- /src/contracts/sample-migration/sample-migrate-token.mp/main.ts: -------------------------------------------------------------------------------- 1 | import { Hex } from "@/types"; 2 | 3 | import { HeliosScript, helios, header, module } from "../../program"; 4 | 5 | export type Params = { 6 | governorPkh: Hex; 7 | }; 8 | 9 | export default function main({ governorPkh }: Params): HeliosScript { 10 | return helios` 11 | ${header("minting", "mp__sample_migrate_token_policy")} 12 | 13 | import { is_tx_authorized_by } 14 | from ${module("helpers")} 15 | 16 | const SAMPLE_GOVERNOR_CREDENTIAL: Credential = 17 | Credential::new_pubkey( 18 | PubKeyHash::new(#${governorPkh}) 19 | ) 20 | 21 | func main(_, ctx: ScriptContext) -> Bool { 22 | tx: Tx = ctx.tx; 23 | 24 | is_tx_authorized_by(tx, SAMPLE_GOVERNOR_CREDENTIAL) 25 | } 26 | `; 27 | } 28 | -------------------------------------------------------------------------------- /src/contracts/treasury/shared-treasury.v/types.ts: -------------------------------------------------------------------------------- 1 | import { header, helios, module } from "../../program"; 2 | 3 | export default helios` 4 | ${header("module", "v__shared_treasury__types")} 5 | 6 | import { TreasuryTag } 7 | from ${module("common__types")} 8 | 9 | enum ProjectTeiki { 10 | TeikiEmpty 11 | TeikiBurntPeriodically { 12 | available: Int 13 | last_burn_at: Time 14 | } 15 | TeikiBurntEntirely 16 | } 17 | 18 | struct Datum { 19 | project_id: ByteArray 20 | governor_teiki: Int 21 | project_teiki: ProjectTeiki 22 | tag: TreasuryTag 23 | } 24 | 25 | enum BurnAction { 26 | BurnPeriodically 27 | BurnEntirely 28 | } 29 | 30 | enum Redeemer { 31 | UpdateTeiki { 32 | burn_action: BurnAction 33 | burn_amount: Int 34 | rewards: Int 35 | } 36 | Migrate 37 | } 38 | `; 39 | -------------------------------------------------------------------------------- /src/commands/utils.ts: -------------------------------------------------------------------------------- 1 | import { Lucid, Network, Blockfrost } from "lucid-cardano"; 2 | 3 | export async function getLucid(): Promise { 4 | const BLOCKFROST_URL = requiredEnv("BLOCKFROST_URL"); 5 | const BLOCKFROST_PROJECT_ID = requiredEnv("BLOCKFROST_PROJECT_ID"); 6 | const NETWORK = requiredEnv("NETWORK") as Network; 7 | const TEST_SEED_PHRASE_URL = process.env["TEST_SEED_PHRASE_URL"]; 8 | 9 | const blockfrostProvider = new Blockfrost( 10 | BLOCKFROST_URL, 11 | BLOCKFROST_PROJECT_ID 12 | ); 13 | const lucid = await Lucid.new(blockfrostProvider, NETWORK); 14 | if (TEST_SEED_PHRASE_URL) 15 | lucid.selectWalletFromSeed(decodeURIComponent(TEST_SEED_PHRASE_URL)); 16 | return lucid; 17 | } 18 | 19 | export function requiredEnv(key: string): string { 20 | const value = process.env[key]; 21 | if (value) return value; 22 | else throw new Error(`${key} must be set`); 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowUnreachableCode": false, 4 | "allowUnusedLabels": false, 5 | "declaration": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "lib": ["es2020"], 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "noEmitOnError": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitReturns": true, 15 | "pretty": true, 16 | "sourceMap": true, 17 | "strict": true, 18 | "target": "es2020", 19 | "outDir": "dist", 20 | "baseUrl": ".", 21 | "paths": { 22 | "@/*": ["src/*"] 23 | } 24 | }, 25 | "ts-node": { 26 | "esm": true, 27 | "experimentalSpecifierResolution": "node", 28 | "require": ["tsconfig-paths/register"] 29 | }, 30 | "include": ["src", "tests"], 31 | "exclude": ["node_modules", "dist"] 32 | } 33 | -------------------------------------------------------------------------------- /src/contracts/project/project.v/types.ts: -------------------------------------------------------------------------------- 1 | import { header, helios } from "../../program"; 2 | 3 | export default helios` 4 | ${header("module", "v__project__types")} 5 | 6 | enum ProjectStatus { 7 | Active 8 | PreClosed { pending_until: Time } 9 | PreDelisted { pending_until: Time } 10 | Closed { closed_at: Time } 11 | Delisted { delisted_at: Time } 12 | } 13 | 14 | struct Datum { 15 | project_id: ByteArray 16 | owner_address: Address 17 | status: ProjectStatus 18 | milestone_reached: Int 19 | is_staking_delegation_managed_by_protocol: Bool 20 | } 21 | 22 | enum Redeemer { 23 | RecordNewMilestone { new_milestone: Int } 24 | AllocateStakingValidator { new_staking_validator: StakingValidatorHash } 25 | UpdateStakingDelegationManagement 26 | InitiateClose 27 | FinalizeClose 28 | InitiateDelist 29 | CancelDelist 30 | FinalizeDelist 31 | Migrate 32 | } 33 | `; 34 | -------------------------------------------------------------------------------- /src/contracts/common/types.ts: -------------------------------------------------------------------------------- 1 | import { header, helios } from "../program"; 2 | 3 | export default helios` 4 | ${header("module", "common__types")} 5 | 6 | enum UserTag { 7 | TagProjectFundsWithdrawal { project_id: ByteArray } 8 | TagProjectClosed { project_id: ByteArray } 9 | TagProjectScriptClosed { 10 | project_id: ByteArray 11 | staking_validator: StakingValidatorHash 12 | } 13 | TagInactiveBacking { backing_output_id: TxOutputId } 14 | TagTreasuryWithdrawal { treasury_output_id: TxOutputId } 15 | } 16 | 17 | enum TreasuryTag { 18 | TagOriginated { seed: TxOutputId } 19 | TagContinuation { former: TxOutputId } 20 | TagProtocolStakingRewards { staking_validator: StakingValidatorHash } 21 | TagProjectDelayedStakingRewards { staking_validator: Option[StakingValidatorHash] } 22 | TagProjectDelisted { project_id: ByteArray } 23 | TagProjectScriptDelisted { 24 | project_id: ByteArray 25 | staking_validator: StakingValidatorHash 26 | } 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /src/contracts/protocol/protocol-script.v/main.ts: -------------------------------------------------------------------------------- 1 | import { Hex } from "@/types"; 2 | 3 | import { header, helios, HeliosScript, module } from "../../program"; 4 | 5 | export type Params = { 6 | protocolNftMph: Hex; 7 | }; 8 | 9 | export default function main({ protocolNftMph }: Params): HeliosScript { 10 | return helios` 11 | ${header("spending", "v__protocol_script")} 12 | 13 | import { 14 | is_tx_authorized_by, 15 | find_pparams_datum_from_inputs 16 | } from ${module("helpers")} 17 | 18 | import { Datum as PParamsDatum } 19 | from ${module("v__protocol_params__types")} 20 | 21 | const PROTOCOL_NFT_MPH: MintingPolicyHash = 22 | MintingPolicyHash::new(#${protocolNftMph}) 23 | 24 | func main(_, _, ctx: ScriptContext) -> Bool { 25 | tx: Tx = ctx.tx; 26 | 27 | pparams_datum: PParamsDatum = 28 | find_pparams_datum_from_inputs(tx.ref_inputs, PROTOCOL_NFT_MPH); 29 | 30 | is_tx_authorized_by(tx, pparams_datum.governor_address.credential) 31 | } 32 | `; 33 | } 34 | -------------------------------------------------------------------------------- /src/transactions/meta-protocol/burn-teiki.ts: -------------------------------------------------------------------------------- 1 | import { Lucid, UTxO, Unit } from "lucid-cardano"; 2 | 3 | import { TEIKI_TOKEN_NAME } from "@/contracts/common/constants"; 4 | import * as S from "@/schema"; 5 | import { TeikiMintingRedeemer } from "@/schema/teiki/meta-protocol"; 6 | import { assert } from "@/utils"; 7 | 8 | export type Params = { 9 | teikiMpRefScriptUtxo: UTxO; 10 | burnAmount: bigint; 11 | }; 12 | 13 | export function burnTeikiTx( 14 | lucid: Lucid, 15 | { teikiMpRefScriptUtxo, burnAmount }: Params 16 | ) { 17 | assert( 18 | teikiMpRefScriptUtxo.scriptRef != null, 19 | "Invalid teiki minting policy reference UTxO: must reference teiki minting policy script" 20 | ); 21 | 22 | const teikiMph = lucid.utils.validatorToScriptHash( 23 | teikiMpRefScriptUtxo.scriptRef 24 | ); 25 | 26 | const teikiUnit: Unit = teikiMph + TEIKI_TOKEN_NAME; 27 | 28 | return lucid 29 | .newTx() 30 | .readFrom([teikiMpRefScriptUtxo]) 31 | .mintAssets( 32 | { [teikiUnit]: -burnAmount }, 33 | S.toCbor(S.toData({ case: "Burn" }, TeikiMintingRedeemer)) 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/transactions/protocol/reclaim-scripts.ts: -------------------------------------------------------------------------------- 1 | import { Data, Lucid, UTxO } from "lucid-cardano"; 2 | 3 | import { extractPaymentPubKeyHash } from "@/helpers/schema"; 4 | import * as S from "@/schema"; 5 | import { ProtocolParamsDatum } from "@/schema/teiki/protocol"; 6 | import { assert } from "@/utils"; 7 | 8 | export type Params = { 9 | protocolParamsUtxo: UTxO; 10 | reclaimUtxos: UTxO[]; 11 | protocolScriptVRefScriptUtxo: UTxO; 12 | }; 13 | 14 | export function reclaimProtocolScriptTx( 15 | lucid: Lucid, 16 | { protocolParamsUtxo, reclaimUtxos, protocolScriptVRefScriptUtxo }: Params 17 | ) { 18 | assert(protocolParamsUtxo.datum, "Protocol params utxo must have datum"); 19 | const protocolParams = S.fromData( 20 | S.fromCbor(protocolParamsUtxo.datum), 21 | ProtocolParamsDatum 22 | ); 23 | 24 | const protocolGovernorPkh = extractPaymentPubKeyHash( 25 | protocolParams.governorAddress 26 | ); 27 | 28 | return lucid 29 | .newTx() 30 | .readFrom([protocolParamsUtxo, protocolScriptVRefScriptUtxo]) 31 | .addSignerKey(protocolGovernorPkh) 32 | .collectFrom(reclaimUtxos, Data.void()); 33 | } 34 | -------------------------------------------------------------------------------- /src/transactions/meta-protocol/evolve-teiki.ts: -------------------------------------------------------------------------------- 1 | import { Lucid, UTxO, Unit } from "lucid-cardano"; 2 | 3 | import { TEIKI_TOKEN_NAME } from "@/contracts/common/constants"; 4 | import * as S from "@/schema"; 5 | import { TeikiMintingRedeemer } from "@/schema/teiki/meta-protocol"; 6 | import { assert } from "@/utils"; 7 | 8 | export type Params = { 9 | teikiMpRefScriptUtxo: UTxO; 10 | totalTeikiAmount: bigint; // total 11 | }; 12 | 13 | export function evolveTeikiTx( 14 | lucid: Lucid, 15 | { teikiMpRefScriptUtxo, totalTeikiAmount }: Params 16 | ) { 17 | assert( 18 | teikiMpRefScriptUtxo.scriptRef != null, 19 | "Invalid teiki minting policy reference UTxO: must reference teiki minting policy script" 20 | ); 21 | 22 | const teikiMph = lucid.utils.validatorToScriptHash( 23 | teikiMpRefScriptUtxo.scriptRef 24 | ); 25 | 26 | const teikiUnit: Unit = teikiMph + TEIKI_TOKEN_NAME; 27 | 28 | return lucid 29 | .newTx() 30 | .readFrom([teikiMpRefScriptUtxo]) 31 | .mintAssets( 32 | { [teikiUnit]: -totalTeikiAmount }, 33 | S.toCbor(S.toData({ case: "Evolve" }, TeikiMintingRedeemer)) 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/transactions/constants.ts: -------------------------------------------------------------------------------- 1 | export const MIN_UTXO_LOVELACE = 2_000_000n; 2 | 3 | export const INACTIVE_PROJECT_UTXO_ADA = 2_000_000n; 4 | 5 | export const PROJECT_CLOSE_DISCOUNT_CENTS = 50n; 6 | export const PROJECT_DELIST_DISCOUNT_CENTS = 50n; 7 | export const PROJECT_DETAIL_UTXO_ADA = 2_000_000n; 8 | export const PROJECT_FUNDS_WITHDRAWAL_DISCOUNT_RATIO = 100n; 9 | export const PROJECT_IMMEDIATE_CLOSURE_TX_TIME_SLIPPAGE = 600_000n; 10 | export const PROJECT_NEW_MILESTONE_DISCOUNT_CENTS = 100n; 11 | export const PROJECT_SCRIPT_CLOSE_DISCOUNT_CENTS = 50n; 12 | export const PROJECT_SCRIPT_DELIST_DISCOUNT_CENTS = 50n; 13 | export const PROJECT_SCRIPT_UTXO_ADA = 20_000_000n; 14 | export const PROJECT_SPONSORSHIP_RESOLUTION = 3_600_000n; 15 | 16 | export const TREASURY_MIN_WITHDRAWAL_ADA = 100_000_000n; 17 | export const TREASURY_REVOKE_DISCOUNT_CENTS = 50n; 18 | export const TREASURY_UTXO_MIN_ADA = 2_000_000n; 19 | export const TREASURY_WITHDRAWAL_DISCOUNT_RATIO = 100n; 20 | 21 | export const PROOF_OF_BACKING_PLANT_TX_TIME_SLIPPAGE = 600_000n; 22 | 23 | export const RATIO_MULTIPLIER = BigInt(1e6); 24 | 25 | // TODO: @sk-saru: find the proper number 26 | export const FRACTION_LIMIT = 2_000_000n; 27 | -------------------------------------------------------------------------------- /src/schema/teiki/tags.ts: -------------------------------------------------------------------------------- 1 | import { StakingValidatorHash, TxOutputId } from "../helios"; 2 | import { Enum, Option, Static } from "../uplc"; 3 | 4 | import { ProjectId } from "./common"; 5 | 6 | export const UserTag = Enum("kind", { 7 | TagProjectFundsWithdrawal: { projectId: ProjectId }, 8 | TagProjectClosed: { projectId: ProjectId }, 9 | TagProjectScriptClosed: { 10 | projectId: ProjectId, 11 | stakingValidator: StakingValidatorHash, 12 | }, 13 | TagInactiveBacking: { backingOutputId: TxOutputId }, 14 | TagTreasuryWithdrawal: { treasuryOutputId: TxOutputId }, 15 | }); 16 | export type UserTag = Static; 17 | 18 | export const TreasuryTag = Enum("kind", { 19 | TagOriginated: { seed: TxOutputId }, 20 | TagContinuation: { former: TxOutputId }, 21 | TagProtocolStakingRewards: { stakingValidator: StakingValidatorHash }, 22 | TagProjectDelayedStakingRewards: { 23 | stakingValidator: Option(StakingValidatorHash), 24 | }, 25 | TagProjectDelisted: { projectId: ProjectId }, 26 | TagProjectScriptDelisted: { 27 | projectId: ProjectId, 28 | stakingValidator: StakingValidatorHash, 29 | }, 30 | }); 31 | export type TreasuryTag = Static; 32 | -------------------------------------------------------------------------------- /src/contracts/meta-protocol/teiki-plant.v/types.ts: -------------------------------------------------------------------------------- 1 | import { header, helios } from "../../program"; 2 | 3 | export default helios` 4 | ${header("module", "v__teiki_plant__types")} 5 | 6 | enum MintingRedeemer { 7 | Any 8 | ConstrIn { constrs: []Int } 9 | ConstrNotIn { constrs: []Int } 10 | } 11 | 12 | struct MintingPredicate { 13 | minting_policy_hash: MintingPolicyHash 14 | redeemer: MintingRedeemer 15 | } 16 | 17 | struct TokenPredicate { 18 | minting_policy_hash: MintingPolicyHash 19 | token_names: Option[[]ByteArray] 20 | } 21 | 22 | enum Authorization { 23 | MustBe { credential: Credential } 24 | MustHave { predicate: TokenPredicate } 25 | MustMint { predicate: MintingPredicate } 26 | } 27 | 28 | struct Rules { 29 | teiki_minting_rules: []MintingPredicate 30 | proposal_authorizations: []Authorization 31 | proposal_waiting_period: Duration 32 | } 33 | 34 | struct RulesProposal { 35 | in_effect_at: Time 36 | rules: Rules 37 | } 38 | 39 | struct Datum { 40 | rules: Rules 41 | proposal: Option[RulesProposal] 42 | } 43 | 44 | enum Redeemer { 45 | Propose 46 | Apply 47 | Cancel 48 | } 49 | `; 50 | -------------------------------------------------------------------------------- /src/cli/meta-protocol/apply.ts: -------------------------------------------------------------------------------- 1 | import { Unit } from "lucid-cardano"; 2 | 3 | import { getLucid } from "@/commands/utils"; 4 | import { TEIKI_PLANT_NFT_TOKEN_NAME } from "@/contracts/common/constants"; 5 | import { signAndSubmit } from "@/helpers/lucid"; 6 | import { applyMetaProtocolProposalTx } from "@/transactions/meta-protocol/apply"; 7 | import { trimToSlot } from "@/utils"; 8 | 9 | const lucid = await getLucid(); 10 | const teikiPlantNftMph = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; 11 | const teikiPlantNftUnit: Unit = teikiPlantNftMph + TEIKI_PLANT_NFT_TOKEN_NAME; 12 | const teikiPlantAddress = "addr1xxxx"; 13 | 14 | const teikiPlantUtxo = ( 15 | await lucid.utxosAtWithUnit(teikiPlantAddress, teikiPlantNftUnit) 16 | )[0]; 17 | 18 | const teikiPlantScriptUtxo = ( 19 | await lucid.utxosByOutRef([{ txHash: "", outputIndex: 0 }]) 20 | )[0]; 21 | 22 | const txTime = trimToSlot(Date.now()); 23 | 24 | const tx = applyMetaProtocolProposalTx(lucid, { 25 | teikiPlantUtxo, 26 | teikiPlantScriptUtxo, 27 | txTime, 28 | }); 29 | 30 | const txComplete = await tx.complete(); 31 | const txHash = await signAndSubmit(txComplete); 32 | 33 | await lucid.awaitTx(txHash); 34 | 35 | console.log("txHash :>> ", txHash); 36 | -------------------------------------------------------------------------------- /src/contracts/meta-protocol/teiki-plant.nft/main.ts: -------------------------------------------------------------------------------- 1 | import { OutRef } from "@/types"; 2 | 3 | import { HeliosScript, helios, module, header } from "../../program"; 4 | 5 | export type Params = { 6 | teikiPlantSeed: OutRef; 7 | }; 8 | 9 | export default function main({ teikiPlantSeed }: Params): HeliosScript { 10 | return helios` 11 | ${header("minting", "nft__teiki_plant")} 12 | 13 | import { TEIKI_PLANT_NFT_TOKEN_NAME } 14 | from ${module("constants")} 15 | 16 | import { Redeemer } 17 | from ${module("nft__teiki_plant__types")} 18 | 19 | const SEED_OUTPUT_ID: TxOutputId = 20 | TxOutputId::new( 21 | TxId::new(#${teikiPlantSeed.txHash}), 22 | ${teikiPlantSeed.outputIndex} 23 | ) 24 | 25 | func main(redeemer: Redeemer, ctx: ScriptContext) -> Bool { 26 | tx: Tx = ctx.tx; 27 | own_mph: MintingPolicyHash = ctx.get_current_minting_policy_hash(); 28 | 29 | redeemer.switch { 30 | 31 | Bootstrap => { 32 | tx.inputs.any((input: TxInput) -> { input.output_id == SEED_OUTPUT_ID }) 33 | && tx.minted.get_policy(own_mph) == Map[ByteArray]Int {TEIKI_PLANT_NFT_TOKEN_NAME: 1} 34 | } 35 | 36 | } 37 | } 38 | `; 39 | } 40 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // Note that the `string` case of Lucid `Data` is in hex form. 2 | import { Constr, type Data, type OutRef } from "lucid-cardano"; 3 | 4 | export { type Data, type OutRef }; 5 | 6 | export type Actor = 7 | | "protocol-governor" 8 | | "staking-manager" 9 | | "project-owner" 10 | | "anyone"; 11 | 12 | export type Hex = string; 13 | export type Cid = string; 14 | 15 | export type UnixTime = number; 16 | export type TimeDifference = number; 17 | 18 | export type LovelaceAmount = number | bigint; 19 | 20 | // Guards 21 | export function isHex(value: unknown): value is Hex { 22 | return typeof value === "string" && /^[0-9A-Fa-f]*$/.test(value); 23 | } 24 | 25 | export function isData(value: unknown): value is Data { 26 | return ( 27 | typeof value === "bigint" || 28 | isHex(value) || 29 | (value instanceof Array && value.every(isData)) || 30 | (value instanceof Map && 31 | Array.from(value.entries()).every(([k, v]) => isData(k) && isData(v))) || 32 | value instanceof Constr 33 | ); 34 | } 35 | 36 | export function isEmpty(value: unknown): value is Record { 37 | return ( 38 | typeof value === "object" && value !== null && !Object.keys(value).length 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { pathsToModuleNameMapper } from "ts-jest"; 2 | 3 | // In the following statement, replace `./tsconfig` with the path to your `tsconfig` file 4 | // which contains the path mapping (ie the `compilerOptions.paths` option): 5 | import { compilerOptions } from "./tsconfig.json"; 6 | 7 | import type { JestConfigWithTsJest } from "ts-jest"; 8 | 9 | const esModules = ["@hyperionbt/helios"].join("|"); 10 | 11 | const jestConfig: JestConfigWithTsJest = { 12 | testEnvironment: "node", 13 | roots: [""], 14 | extensionsToTreatAsEsm: [".ts"], 15 | testPathIgnorePatterns: ["/node_modules/", "/dist/"], 16 | modulePaths: [compilerOptions.baseUrl], 17 | moduleNameMapper: { 18 | "(bignumber\\.js)": "$1", 19 | "(.+)\\.[jt]sx?": "$1", 20 | ...pathsToModuleNameMapper(compilerOptions.paths, { useESM: true }), 21 | }, 22 | transformIgnorePatterns: [`node_modules/(?!${esModules})`], 23 | transform: { 24 | "^.+\\.[tj]sx?$": [ 25 | "ts-jest", 26 | { 27 | tsconfig: "tsconfig.test.json", 28 | isolatedModules: true, 29 | useESM: true, 30 | }, 31 | ], 32 | }, 33 | setupFiles: ["/tests/setup.ts"], 34 | }; 35 | 36 | export default jestConfig; 37 | -------------------------------------------------------------------------------- /src/cli/bootstrap-kolour-nft.ts: -------------------------------------------------------------------------------- 1 | import { getLucid, requiredEnv } from "@/commands/utils"; 2 | import { exportScript, compile } from "@/contracts/compile"; 3 | import getKolourNft from "@/contracts/kolours/kolour.nft/main"; 4 | 5 | import { alwaysFalse, deployReferencedScript, printScriptHash } from "./utils"; 6 | 7 | const lucid = await getLucid(); 8 | const kolourPkh = requiredEnv("KOLOUR_NFT_PUB_KEY_HASH"); 9 | const kreationPkh = requiredEnv("KREATION_NFT_PUB_KEY_HASH"); 10 | 11 | const alwaysFalseVScript = exportScript(compile(alwaysFalse())); 12 | const alwaysFalseAddress = lucid.utils.validatorToAddress(alwaysFalseVScript); 13 | console.log( 14 | "ALWAYS_FALSE_SCRIPT_HASH", 15 | lucid.utils.validatorToScriptHash(alwaysFalseVScript) 16 | ); 17 | 18 | const kolourNftScript = exportScript( 19 | compile(getKolourNft({ producerPkh: kolourPkh })) 20 | ); 21 | 22 | const genesisKreationNftScript = exportScript( 23 | compile(getKolourNft({ producerPkh: kreationPkh })) 24 | ); 25 | 26 | const scripts = { 27 | KOLOUR_NFT_MPH: kolourNftScript, 28 | GENESIS_KREATION_NFT_MPH: genesisKreationNftScript, 29 | }; 30 | 31 | await deployReferencedScript(lucid, Object.values(scripts), alwaysFalseAddress); 32 | 33 | printScriptHash(lucid, scripts); 34 | -------------------------------------------------------------------------------- /src/helpers/helios.ts: -------------------------------------------------------------------------------- 1 | import * as helios from "@hyperionbt/helios"; 2 | 3 | // helper functions for script property tests 4 | export function asBool(value: helios.UplcData | helios.UplcValue): boolean { 5 | if (value instanceof helios.UplcBool) { 6 | return value.bool; 7 | } else if (value instanceof helios.ConstrData) { 8 | if (value.fields.length == 0) { 9 | if (value.index == 0) { 10 | return false; 11 | } else if (value.index == 1) { 12 | return true; 13 | } else { 14 | throw new Error( 15 | `unexpected ConstrData index ${value.index} (expected 0 or 1 for Bool)` 16 | ); 17 | } 18 | } else { 19 | throw new Error(`expected ConstrData with 0 fields (Bool)`); 20 | } 21 | } else if (value instanceof helios.UplcDataValue) { 22 | return asBool(value.data); 23 | } 24 | 25 | throw new Error(`expected UplcBool, got ${value.toString()}`); 26 | } 27 | 28 | export function asInt(value: helios.UplcValue) { 29 | if (value instanceof helios.IntData) { 30 | return value.value; 31 | } else if (value instanceof helios.UplcDataValue) { 32 | const data = value.data; 33 | if (data instanceof helios.IntData) { 34 | return data.value; 35 | } 36 | } 37 | 38 | throw new Error(`expected IntData, got ${value.toString()}`); 39 | } 40 | -------------------------------------------------------------------------------- /src/transactions/deploy-scripts.ts: -------------------------------------------------------------------------------- 1 | import { Address, Data, Lucid, Script } from "lucid-cardano"; 2 | 3 | export type DeployScriptParams = { 4 | deployAddress: Address; 5 | scriptReferences: Script[]; 6 | batchScriptSize: number; 7 | }; 8 | 9 | function scriptSize(script: Script) { 10 | return Math.floor(script.script.length / 2); 11 | } 12 | 13 | function partition(scripts: Script[], batchScriptSize: number): Script[][] { 14 | const result: Script[][] = []; 15 | for (const script of scripts) { 16 | const batch = result.find( 17 | (row) => 18 | row.reduce( 19 | (sumScriptSize, item) => sumScriptSize + scriptSize(item), 20 | 0 21 | ) + 22 | scriptSize(script) <= 23 | batchScriptSize 24 | ); 25 | if (batch) batch.push(script); 26 | else result.push([script]); 27 | } 28 | return result; 29 | } 30 | 31 | export function deployScriptsTx( 32 | lucid: Lucid, 33 | { deployAddress, scriptReferences, batchScriptSize }: DeployScriptParams 34 | ) { 35 | return partition(scriptReferences, batchScriptSize).map((scripts) => 36 | scripts.reduce( 37 | (tx, script) => 38 | tx.payToContract( 39 | deployAddress, 40 | { inline: Data.void(), scriptRef: script }, 41 | {} 42 | ), 43 | lucid.newTx() 44 | ) 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/schema/teiki/backing.ts: -------------------------------------------------------------------------------- 1 | import { Address, Time, TxOutputId } from "../helios"; 2 | import { Bool, Enum, Int, List, Static, Struct, Void } from "../uplc"; 3 | 4 | import { ProjectId } from "./common"; 5 | 6 | // ==================== V | Backing ==================== 7 | 8 | export const BackingDatum = Struct({ 9 | projectId: ProjectId, 10 | backerAddress: Address, 11 | backedAt: Time, 12 | milestoneBacked: Int, 13 | }); 14 | export type BackingDatum = Static; 15 | 16 | export const BackingRedeemer = Enum("case", { 17 | Unback: Void, 18 | Migrate: Void, 19 | }); 20 | export type BackingRedeemer = Static; 21 | 22 | // ==================== NFT | Proof of Backing ==================== 23 | 24 | export const Plant = Struct({ 25 | isMatured: Bool, 26 | backingOutputId: TxOutputId, 27 | backingAmount: Int, 28 | unbackedAt: Time, 29 | projectId: ProjectId, 30 | backerAddress: Address, 31 | backedAt: Time, 32 | milestoneBacked: Int, 33 | }); 34 | export type Plant = Static; 35 | 36 | export const ProofOfBackingMintingRedeemer = Enum("case", { 37 | Plant: { cleanup: Bool }, 38 | ClaimRewards: { flowers: List(Plant) }, 39 | MigrateOut: Void, 40 | MigrateIn: Void, 41 | Burn: Void, 42 | }); 43 | export type ProofOfBackingMintingRedeemer = Static< 44 | typeof ProofOfBackingMintingRedeemer 45 | >; 46 | -------------------------------------------------------------------------------- /src/transactions/fraction.ts: -------------------------------------------------------------------------------- 1 | // This is a mirror of `src/contracts/common/fraction.ts` 2 | 3 | import { FRACTION_LIMIT } from "./constants"; 4 | 5 | export type Fraction = { 6 | numerator: bigint; 7 | denominator: bigint; 8 | }; 9 | 10 | export function multiplyFraction(f1: Fraction, f2: Fraction): Fraction { 11 | const temp: Fraction = { 12 | numerator: f1.numerator * f2.numerator, 13 | denominator: f1.denominator * f2.denominator, 14 | }; 15 | 16 | if (temp.numerator + temp.denominator <= FRACTION_LIMIT) { 17 | return temp; 18 | } else { 19 | return { 20 | numerator: 21 | (temp.numerator * FRACTION_LIMIT) / (temp.numerator + temp.denominator), 22 | denominator: 23 | (temp.denominator * FRACTION_LIMIT) / 24 | (temp.numerator + temp.denominator), 25 | }; 26 | } 27 | } 28 | 29 | export function exponentialFraction(f: Fraction, exponent: number): Fraction { 30 | if (exponent === 0) { 31 | return { numerator: 1n, denominator: 1n }; 32 | } else if (exponent === 1) { 33 | return f; 34 | } else if (exponent % 2 === 0) { 35 | const half: Fraction = exponentialFraction(f, exponent / 2); 36 | return multiplyFraction(half, half); 37 | } else { 38 | const half: Fraction = exponentialFraction(f, Math.floor(exponent / 2)); 39 | return multiplyFraction(f, multiplyFraction(half, half)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/compile-scripts/project/project.sv.test.ts: -------------------------------------------------------------------------------- 1 | import { bytesToHex } from "@hyperionbt/helios"; 2 | 3 | import { compile } from "@/contracts/compile"; 4 | import getProjectSv from "@/contracts/project/project.sv/main"; 5 | 6 | import { compileAndLog } from "../base"; 7 | 8 | test("compile: SV | Project", () => { 9 | const size = compileAndLog( 10 | getProjectSv({ 11 | projectId: "", 12 | stakingSeed: "", 13 | projectAtMph: "", 14 | protocolNftMph: "", 15 | }) 16 | ); 17 | expect(size).toBeGreaterThan(0); 18 | }); 19 | 20 | test("compile: SV | Project with different stakingSeed", () => { 21 | function compareStakingCredential( 22 | seed1: string, 23 | seed2: string, 24 | simplify = true 25 | ) { 26 | const projectId = ""; 27 | const projectAtMph = ""; 28 | const protocolNftMph = ""; 29 | function _compile(seed: string) { 30 | const uplcProgram = compile( 31 | getProjectSv({ 32 | projectId, 33 | stakingSeed: seed, 34 | projectAtMph, 35 | protocolNftMph, 36 | }), 37 | { simplify } 38 | ); 39 | return bytesToHex(uplcProgram.hash()); 40 | } 41 | return _compile(seed1) === _compile(seed2); 42 | } 43 | expect(compareStakingCredential("abc", "abc")).toBeTruthy(); 44 | expect(compareStakingCredential("abc", "def")).toBeFalsy(); 45 | }); 46 | -------------------------------------------------------------------------------- /src/transactions/meta-data.ts: -------------------------------------------------------------------------------- 1 | import { Tx } from "lucid-cardano"; 2 | 3 | import { Hex } from "@/types"; 4 | 5 | export const TEIKI_METADATA_LABEL = 731211; 6 | 7 | // IPFS content identification 8 | export type Cid = string; 9 | 10 | export type Params = { 11 | policyId: Hex; 12 | assetName: Hex; 13 | nftName: "Teiki Hana" | "Teiki Kuda"; 14 | projectId: Hex; 15 | backingAmount: bigint; 16 | duration: bigint; 17 | imageCid?: string; 18 | }; 19 | 20 | export function attachTeikiNftMetadata( 21 | tx: Tx, 22 | { 23 | policyId, 24 | assetName, 25 | nftName, 26 | imageCid, 27 | projectId, 28 | backingAmount, 29 | duration, 30 | }: Params 31 | ) { 32 | const metadata = { 33 | [policyId]: { 34 | [assetName]: { 35 | name: nftName, 36 | image: `ipfs://${ 37 | imageCid ? imageCid : "QmSwqXzXVVZX6XWkWe6WbKGor7vYj8MGJHLFWJ3aSCdreV" 38 | }`, 39 | description: "The Proof of Backing NFT on Teiki protocol", 40 | project_id: projectId, 41 | backing_amount: Number(backingAmount), 42 | duration: Number(duration), 43 | }, 44 | }, 45 | version: 2, // asset name in hex format 46 | }; 47 | return tx.attachMetadata(721, metadata); 48 | } 49 | 50 | export function getPlantNftName({ isMatured }: { isMatured: boolean }) { 51 | return isMatured ? "Teiki Kuda" : "Teiki Hana"; 52 | } 53 | -------------------------------------------------------------------------------- /src/transactions/meta-protocol/cancel.ts: -------------------------------------------------------------------------------- 1 | import { Lucid, UTxO } from "lucid-cardano"; 2 | 3 | import * as S from "@/schema"; 4 | import { 5 | TeikiPlantDatum, 6 | TeikiPlantRedeemer, 7 | } from "@/schema/teiki/meta-protocol"; 8 | import { assert } from "@/utils"; 9 | 10 | export type CancelMetaProtocolTxParams = { 11 | teikiPlantUtxo: UTxO; 12 | teikiPlantScriptUtxo: UTxO; 13 | }; 14 | 15 | export function cancelMetaProtocolProposalTx( 16 | lucid: Lucid, 17 | { teikiPlantUtxo, teikiPlantScriptUtxo }: CancelMetaProtocolTxParams 18 | ) { 19 | assert( 20 | teikiPlantUtxo.datum, 21 | "Invalid Teiki plant UTxO: Missing inline datum" 22 | ); 23 | 24 | const teikiPlantDatum = S.fromData( 25 | S.fromCbor(teikiPlantUtxo.datum), 26 | TeikiPlantDatum 27 | ); 28 | 29 | const teikiPlantRedeemer: TeikiPlantRedeemer = { case: "Cancel" }; 30 | 31 | const canceledTeikiPlantDatum: TeikiPlantDatum = { 32 | rules: teikiPlantDatum.rules, 33 | proposal: null, 34 | }; 35 | 36 | return lucid 37 | .newTx() 38 | .readFrom([teikiPlantScriptUtxo]) 39 | .collectFrom( 40 | [teikiPlantUtxo], 41 | S.toCbor(S.toData(teikiPlantRedeemer, TeikiPlantRedeemer)) 42 | ) 43 | .payToContract( 44 | teikiPlantUtxo.address, 45 | { inline: S.toCbor(S.toData(canceledTeikiPlantDatum, TeikiPlantDatum)) }, 46 | teikiPlantUtxo.assets 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/transactions/project/update-staking-delegation-management.ts: -------------------------------------------------------------------------------- 1 | import { Lucid, UTxO } from "lucid-cardano"; 2 | 3 | import * as S from "@/schema"; 4 | import { ProjectDatum, ProjectRedeemer } from "@/schema/teiki/project"; 5 | import { assert } from "@/utils"; 6 | 7 | export type Params = { 8 | protocolParamsUtxo: UTxO; 9 | projectUtxo: UTxO; 10 | projectVRefScriptUtxo: UTxO; 11 | }; 12 | 13 | export function updateStakingDelegationManagement( 14 | lucid: Lucid, 15 | { protocolParamsUtxo, projectUtxo, projectVRefScriptUtxo }: Params 16 | ) { 17 | assert( 18 | projectUtxo.datum != null, 19 | "Invalid project UTxO: Missing inline datum" 20 | ); 21 | const projectDatum = S.fromData(S.fromCbor(projectUtxo.datum), ProjectDatum); 22 | 23 | const projectRedeemer: ProjectRedeemer = { 24 | case: "UpdateStakingDelegationManagement", 25 | }; 26 | 27 | const outputProjectDatum: ProjectDatum = { 28 | ...projectDatum, 29 | isStakingDelegationManagedByProtocol: false, 30 | }; 31 | 32 | return lucid 33 | .newTx() 34 | .readFrom([protocolParamsUtxo, projectVRefScriptUtxo]) 35 | .collectFrom( 36 | [projectUtxo], 37 | S.toCbor(S.toData(projectRedeemer, ProjectRedeemer)) 38 | ) 39 | .payToContract( 40 | projectUtxo.address, 41 | { 42 | inline: S.toCbor(S.toData(outputProjectDatum, ProjectDatum)), 43 | }, 44 | { ...projectUtxo.assets } 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/contracts/common/fraction.ts: -------------------------------------------------------------------------------- 1 | // Synchronize with the fraction in transactions 2 | import { FRACTION_LIMIT } from "@/transactions/constants"; 3 | 4 | import { header, helios } from "../program"; 5 | 6 | export default helios` 7 | ${header("module", "fraction")} 8 | 9 | const FRACTION_LIMIT: Int = ${FRACTION_LIMIT} 10 | 11 | struct Fraction { 12 | numerator: Int 13 | denominator: Int 14 | 15 | func multiply(self, other: Fraction) -> Fraction { 16 | t_numerator: Int = self.numerator * other.numerator; 17 | t_denominator: Int = self.denominator * other.denominator; 18 | t_sum: Int = t_numerator + t_denominator; 19 | 20 | if (t_sum > FRACTION_LIMIT) { 21 | Fraction { 22 | numerator: t_numerator * FRACTION_LIMIT / t_sum, 23 | denominator: t_denominator * FRACTION_LIMIT / t_sum 24 | } 25 | } else { 26 | Fraction { 27 | numerator: t_numerator, 28 | denominator: t_denominator 29 | } 30 | } 31 | } 32 | 33 | func exponential(self, exponent: Int) -> Fraction { 34 | if (exponent == 0) { 35 | Fraction { numerator: 1, denominator: 1} 36 | } else if (exponent == 1) { 37 | self 38 | } else if (exponent % 2 == 0) { 39 | half: Fraction = self.exponential(exponent / 2); 40 | half.multiply(half) 41 | } else { 42 | half: Fraction = self.exponential(exponent / 2); 43 | half.multiply(half).multiply(self) 44 | } 45 | } 46 | } 47 | `; 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :warning: [Beta] Kreate Protocol :construction: 2 | 3 | ![Kreate protocol](docs/protocol.png) 4 | 5 | We currently maintain the [Kreate protocol specifications on Notion](https://shinka-network.notion.site/Kreate-Protocol-ae97c4c66db447278ea8da9cd7b860a2). 6 | 7 | This repository contains the implementation in Gen I. It is currently at the Beta stage for mainnet testing. 8 | 9 | ## Getting Started 10 | 11 | ### Installing 12 | 13 | ``` 14 | npm i 15 | ``` 16 | 17 | ### Running the Scripts 18 | 19 | #### Bootstrap protocol 20 | 21 | ```sh 22 | export BLOCKFROST_URL=https://cardano-[preview/preprod/mainnet].blockfrost.io/api/v0 23 | export BLOCKFROST_PROJECT_ID=preview*********************** 24 | export NETWORK=Preview 25 | export TEST_SEED_PHRASE_URL=xxxxx%20xxxxxxx%20xxxxxxxx%20xxxxxxx 26 | export STAKING_MANAGER_ADDRESS=addr_xxxxxxxxxxxxxxx 27 | export POOL_ID=poolxxxxxxxxxxxxxxxxxxxxxxx 28 | ``` 29 | 30 | ``` 31 | npm run deploy 32 | ``` 33 | 34 | ##### Bootstrap kolour NFT 35 | 36 | ```sh 37 | export KOLOUR_NFT_PUB_KEY_HASH=xxxxxxxxxxxxxxxxxxxxxxxx 38 | export KREATION_NFT_PUB_KEY_HASH=xxxxxxxxxxxxxxxxxxxxxxxx 39 | ``` 40 | 41 | #### Propose minting rules 42 | 43 | 1. Update information in `src/cli/meta-protocol/propose.ts` and `src/cli/meta-protocol/apply.ts` then propose: 44 | 45 | ``` 46 | npm run meta-protocol:propose 47 | ``` 48 | 49 | 2. Wait for the proposal duration before applying: 50 | 51 | ``` 52 | npm run meta-protocol:apply 53 | ``` 54 | 55 | #### Emulator Test 56 | 57 | ``` 58 | npm test 59 | ``` 60 | -------------------------------------------------------------------------------- /src/transactions/protocol/cancel.ts: -------------------------------------------------------------------------------- 1 | import { Lucid, UTxO } from "lucid-cardano"; 2 | 3 | import * as S from "@/schema"; 4 | import { 5 | ProtocolParamsDatum, 6 | ProtocolProposalDatum, 7 | ProtocolProposalRedeemer, 8 | } from "@/schema/teiki/protocol"; 9 | import { assert } from "@/utils"; 10 | 11 | import { extractPaymentPubKeyHash } from "../../helpers/schema"; 12 | 13 | export type CancelProtocolTxParams = { 14 | protocolParamsUtxo: UTxO; 15 | protocolProposalUtxo: UTxO; 16 | protocolProposalRefScriptUtxo: UTxO; 17 | }; 18 | 19 | export function cancelProtocolProposalTx( 20 | lucid: Lucid, 21 | { 22 | protocolParamsUtxo, 23 | protocolProposalUtxo, 24 | protocolProposalRefScriptUtxo, 25 | }: CancelProtocolTxParams 26 | ) { 27 | assert(protocolParamsUtxo.datum, "Protocol params utxo must have datum"); 28 | const protocolParams = S.fromData( 29 | S.fromCbor(protocolParamsUtxo.datum), 30 | ProtocolParamsDatum 31 | ); 32 | 33 | const protocolGovernorPkh = extractPaymentPubKeyHash( 34 | protocolParams.governorAddress 35 | ); 36 | 37 | return lucid 38 | .newTx() 39 | .addSignerKey(protocolGovernorPkh) 40 | .readFrom([protocolParamsUtxo, protocolProposalRefScriptUtxo]) 41 | .collectFrom( 42 | [protocolProposalUtxo], 43 | S.toCbor(S.toData({ case: "Cancel" }, ProtocolProposalRedeemer)) 44 | ) 45 | .payToContract( 46 | protocolProposalUtxo.address, 47 | { inline: S.toCbor(S.toData({ proposal: null }, ProtocolProposalDatum)) }, 48 | protocolProposalUtxo.assets 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/contracts/protocol/protocol-params.v/types.ts: -------------------------------------------------------------------------------- 1 | import { header, helios } from "../../program"; 2 | 3 | export default helios` 4 | ${header("module", "v__protocol_params__types")} 5 | 6 | enum Redeemer { 7 | ApplyProposal 8 | } 9 | 10 | struct MigratableScript { 11 | latest: ValidatorHash 12 | migrations: Map[ValidatorHash]AssetClass 13 | } 14 | 15 | struct Registry { 16 | protocol_staking_validator: ScriptHash 17 | project_validator: MigratableScript 18 | project_detail_validator: MigratableScript 19 | project_script_validator: MigratableScript 20 | backing_validator: MigratableScript 21 | dedicated_treasury_validator: MigratableScript 22 | shared_treasury_validator: MigratableScript 23 | open_treasury_validator: MigratableScript 24 | } 25 | 26 | struct Datum { 27 | registry: Registry 28 | governor_address: Address 29 | governor_share_ratio: Int 30 | protocol_funds_share_ratio: Int 31 | discount_cent_price: Int 32 | project_milestones: []Int 33 | teiki_coefficient: Int 34 | project_teiki_burn_rate: Int 35 | epoch_length: Duration 36 | project_pledge: Int 37 | staking_manager: Credential 38 | project_creation_fee: Int 39 | project_sponsorship_min_fee: Int 40 | project_sponsorship_duration: Duration 41 | project_information_update_fee: Int 42 | project_announcement_fee: Int 43 | min_treasury_per_milestone_event: Int 44 | stake_key_deposit: Int 45 | proposal_waiting_period: Duration 46 | project_delist_waiting_period: Duration 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /src/transactions/project/delegate.ts: -------------------------------------------------------------------------------- 1 | import { Address, Data, Lucid, PoolId, UTxO } from "lucid-cardano"; 2 | 3 | import { assert } from "@/utils"; 4 | 5 | // TODO: @sk-yagi: Batching 6 | export type DelegateProjectParams = { 7 | protocolParamsUtxo: UTxO; 8 | authorizedAddress: Address; // Either `StakingManager` or `GovernorAddress` 9 | allDelegatedProjects: DelegateProjectStaking[]; 10 | poolId: PoolId; 11 | }; 12 | 13 | export type DelegateProjectStaking = { 14 | projectUtxo: UTxO; 15 | projectScriptUtxo: UTxO; 16 | }; 17 | 18 | export function delegateProjectTx( 19 | lucid: Lucid, 20 | { 21 | protocolParamsUtxo, 22 | authorizedAddress, 23 | allDelegatedProjects, 24 | poolId, 25 | }: DelegateProjectParams 26 | ) { 27 | let tx = lucid.newTx().readFrom([protocolParamsUtxo]); 28 | 29 | for (const project of allDelegatedProjects) { 30 | assert( 31 | project.projectScriptUtxo.scriptRef != null, 32 | "Invalid project script UTxO" 33 | ); 34 | 35 | const projectSvScriptHash = lucid.utils.validatorToScriptHash( 36 | project.projectScriptUtxo.scriptRef 37 | ); 38 | 39 | const projectStakingCredential = 40 | lucid.utils.scriptHashToCredential(projectSvScriptHash); 41 | const rewardAddress = lucid.utils.credentialToRewardAddress( 42 | projectStakingCredential 43 | ); 44 | 45 | tx = tx 46 | .addSigner(authorizedAddress) 47 | .readFrom([project.projectUtxo, project.projectScriptUtxo]) 48 | .delegateTo(rewardAddress, poolId, Data.void()); 49 | } 50 | 51 | return tx; 52 | } 53 | -------------------------------------------------------------------------------- /src/contracts/kolours/kolour.nft/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | KOLOUR_TX_DEADLINE, 3 | KOLOUR_TX_MAX_DURATION, 4 | } from "@/transactions/kolours/constants"; 5 | import { Hex } from "@/types"; 6 | 7 | import { HeliosScript, header, helios } from "../../program"; 8 | 9 | export type Params = { 10 | producerPkh: Hex; 11 | }; 12 | 13 | export default function main({ producerPkh }: Params): HeliosScript { 14 | return helios` 15 | ${header("minting", "nft__kolour")} 16 | 17 | const TX_TTL: Duration = Duration::new(${KOLOUR_TX_MAX_DURATION.toString()}) 18 | const TX_DEADLINE: Time = Time::new(${KOLOUR_TX_DEADLINE.toString()}) 19 | 20 | const PRODUCER_PKH: PubKeyHash = PubKeyHash::new(#${producerPkh}) 21 | 22 | enum Redeemer { 23 | Mint 24 | Burn 25 | } 26 | 27 | func main(redeemer: Redeemer, ctx: ScriptContext) -> Bool{ 28 | tx: Tx = ctx.tx; 29 | own_mph: MintingPolicyHash = ctx.get_current_minting_policy_hash(); 30 | own_minted: Map[ByteArray]Int = tx.minted.get_policy(own_mph); 31 | 32 | redeemer.switch { 33 | Mint => { 34 | tx_time_start: Time = tx.time_range.start; 35 | tx_time_end: Time = tx.time_range.end; 36 | 37 | own_minted.all( 38 | (_, amount: Int) -> Bool { amount == 1 } 39 | ) 40 | && tx_time_end <= tx_time_start + TX_TTL 41 | && tx_time_end <= TX_DEADLINE 42 | && tx.is_signed_by(PRODUCER_PKH) 43 | }, 44 | Burn => { 45 | own_minted.all( 46 | (_, amount: Int) -> Bool { amount < 0 } 47 | ) 48 | } 49 | } 50 | } 51 | `; 52 | } 53 | -------------------------------------------------------------------------------- /src/cli/protocol/apply.ts: -------------------------------------------------------------------------------- 1 | import { Unit } from "lucid-cardano"; 2 | 3 | import { getLucid } from "@/commands/utils"; 4 | import { PROTOCOL_NFT_TOKEN_NAMES } from "@/contracts/common/constants"; 5 | import { signAndSubmit } from "@/helpers/lucid"; 6 | import { applyProtocolProposalTx } from "@/transactions/protocol/apply"; 7 | import { trimToSlot } from "@/utils"; 8 | 9 | const lucid = await getLucid(); 10 | 11 | const protocolParamsUtxo = ( 12 | await lucid.utxosByOutRef([{ txHash: "", outputIndex: 1 }]) 13 | )[0]; 14 | 15 | const currentProtocolNftMph = 16 | "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; 17 | 18 | const proposalNftUnit: Unit = 19 | currentProtocolNftMph + PROTOCOL_NFT_TOKEN_NAMES.PROPOSAL; 20 | const protocolProposalVAddress = "addr_xxxxx"; 21 | 22 | const protocolProposalUtxo = ( 23 | await lucid.utxosAtWithUnit(protocolProposalVAddress, proposalNftUnit) 24 | )[0]; 25 | const protocolProposalRefScriptUtxo = ( 26 | await lucid.utxosByOutRef([{ txHash: "", outputIndex: 1 }]) 27 | )[0]; 28 | 29 | const protocolParamsRefScriptUtxo = ( 30 | await lucid.utxosByOutRef([{ txHash: "", outputIndex: 1 }]) 31 | )[0]; 32 | 33 | const txTime = trimToSlot(Date.now()); 34 | 35 | const tx = applyProtocolProposalTx(lucid, { 36 | protocolParamsUtxo, 37 | protocolProposalUtxo, 38 | protocolScriptUtxos: [ 39 | protocolParamsRefScriptUtxo, 40 | protocolProposalRefScriptUtxo, 41 | ], 42 | txTime, 43 | }); 44 | 45 | const txComplete = await tx.complete(); 46 | const txHash = await signAndSubmit(txComplete); 47 | 48 | await lucid.awaitTx(txHash); 49 | console.log("txHash :>> ", txHash); 50 | -------------------------------------------------------------------------------- /src/transactions/meta-protocol/apply.ts: -------------------------------------------------------------------------------- 1 | import { Lucid, UTxO } from "lucid-cardano"; 2 | 3 | import * as S from "@/schema"; 4 | import { 5 | TeikiPlantDatum, 6 | TeikiPlantRedeemer, 7 | } from "@/schema/teiki/meta-protocol"; 8 | import { UnixTime } from "@/types"; 9 | import { assert } from "@/utils"; 10 | 11 | export type ApplyMetaProtocolTxParams = { 12 | teikiPlantUtxo: UTxO; 13 | teikiPlantScriptUtxo: UTxO; 14 | txTime: UnixTime; 15 | }; 16 | 17 | export function applyMetaProtocolProposalTx( 18 | lucid: Lucid, 19 | { teikiPlantUtxo, teikiPlantScriptUtxo, txTime }: ApplyMetaProtocolTxParams 20 | ) { 21 | assert( 22 | teikiPlantUtxo.datum != null, 23 | "Invalid Teiki plant UTxO: Missing inline datum" 24 | ); 25 | const teikiPlantDatum = S.fromData( 26 | S.fromCbor(teikiPlantUtxo.datum), 27 | TeikiPlantDatum 28 | ); 29 | 30 | assert( 31 | teikiPlantDatum.proposal, 32 | "Invalid Teiki plant datum: Proposed rule cannot be null" 33 | ); 34 | const teikiPlantRedeemer: TeikiPlantRedeemer = { case: "Apply" }; 35 | 36 | const appliedTeikiPlantDatum: TeikiPlantDatum = { 37 | rules: teikiPlantDatum.proposal.rules, 38 | proposal: null, 39 | }; 40 | 41 | return lucid 42 | .newTx() 43 | .readFrom([teikiPlantScriptUtxo]) 44 | .collectFrom( 45 | [teikiPlantUtxo], 46 | S.toCbor(S.toData(teikiPlantRedeemer, TeikiPlantRedeemer)) 47 | ) 48 | .payToContract( 49 | teikiPlantUtxo.address, 50 | { inline: S.toCbor(S.toData(appliedTeikiPlantDatum, TeikiPlantDatum)) }, 51 | teikiPlantUtxo.assets 52 | ) 53 | .validFrom(txTime); 54 | } 55 | -------------------------------------------------------------------------------- /src/transactions/meta-protocol/propose.ts: -------------------------------------------------------------------------------- 1 | import { Lucid, UTxO } from "lucid-cardano"; 2 | 3 | import * as S from "@/schema"; 4 | import { 5 | RulesProposal, 6 | TeikiPlantDatum, 7 | TeikiPlantRedeemer, 8 | } from "@/schema/teiki/meta-protocol"; 9 | import { TimeDifference } from "@/types"; 10 | import { assert } from "@/utils"; 11 | 12 | export type ProposeMetaProtocolTxParams = { 13 | teikiPlantUtxo: UTxO; 14 | teikiPlantScriptUtxo: UTxO; 15 | proposedRules: RulesProposal; 16 | txValidUntil: TimeDifference; 17 | }; 18 | 19 | export function proposeMetaProtocolProposalTx( 20 | lucid: Lucid, 21 | { 22 | teikiPlantUtxo, 23 | teikiPlantScriptUtxo, 24 | proposedRules, 25 | txValidUntil, 26 | }: ProposeMetaProtocolTxParams 27 | ) { 28 | assert( 29 | teikiPlantUtxo.datum != null, 30 | "Invalid Teiki plant UTxO: Missing inline datum" 31 | ); 32 | 33 | const teikiPlantDatum = S.fromData( 34 | S.fromCbor(teikiPlantUtxo.datum), 35 | TeikiPlantDatum 36 | ); 37 | 38 | const teikiPlantRedeemer: TeikiPlantRedeemer = { case: "Propose" }; 39 | 40 | const newTeikiPlantDatum: TeikiPlantDatum = { 41 | ...teikiPlantDatum, 42 | proposal: proposedRules, 43 | }; 44 | 45 | return lucid 46 | .newTx() 47 | .readFrom([teikiPlantScriptUtxo]) 48 | .collectFrom( 49 | [teikiPlantUtxo], 50 | S.toCbor(S.toData(teikiPlantRedeemer, TeikiPlantRedeemer)) 51 | ) 52 | .payToContract( 53 | teikiPlantUtxo.address, 54 | { inline: S.toCbor(S.toData(newTeikiPlantDatum, TeikiPlantDatum)) }, 55 | teikiPlantUtxo.assets 56 | ) 57 | .validTo(txValidUntil); 58 | } 59 | -------------------------------------------------------------------------------- /src/transactions/meta-protocol/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { Address, Lucid, PolicyId, Script, Unit, UTxO } from "lucid-cardano"; 2 | 3 | import { TEIKI_PLANT_NFT_TOKEN_NAME } from "@/contracts/common/constants"; 4 | import * as S from "@/schema"; 5 | import { 6 | TeikiPlantDatum, 7 | TeikiPlantNftMintingRedeemer, 8 | } from "@/schema/teiki/meta-protocol"; 9 | import { TimeDifference, UnixTime } from "@/types"; 10 | 11 | export type BootstrapMetaProtocolTxParams = { 12 | seedUtxo: UTxO; 13 | teikiPlantDatum: TeikiPlantDatum; 14 | teikiPlantNftPolicy: Script; 15 | teikiPlantAddress: Address; 16 | txTime: UnixTime; 17 | txTtl?: TimeDifference; 18 | }; 19 | 20 | export function bootstrapMetaProtocolTx( 21 | lucid: Lucid, 22 | { 23 | teikiPlantDatum, 24 | seedUtxo, 25 | teikiPlantNftPolicy, 26 | teikiPlantAddress, 27 | txTime, 28 | txTtl = 600_000, 29 | }: BootstrapMetaProtocolTxParams 30 | ) { 31 | const teikiPlantNftPolicyId: PolicyId = 32 | lucid.utils.mintingPolicyToId(teikiPlantNftPolicy); 33 | const teikiPlantNftUnit: Unit = 34 | teikiPlantNftPolicyId + TEIKI_PLANT_NFT_TOKEN_NAME; 35 | 36 | return lucid 37 | .newTx() 38 | .collectFrom([seedUtxo]) 39 | .mintAssets( 40 | { [teikiPlantNftUnit]: 1n }, 41 | S.toCbor(S.toData({ case: "Bootstrap" }, TeikiPlantNftMintingRedeemer)) 42 | ) 43 | .attachMintingPolicy(teikiPlantNftPolicy) 44 | .payToContract( 45 | teikiPlantAddress, 46 | { inline: S.toCbor(S.toData(teikiPlantDatum, TeikiPlantDatum)) }, 47 | { [teikiPlantNftUnit]: 1n } 48 | ) 49 | .validFrom(txTime) 50 | .validTo(txTime + txTtl); 51 | } 52 | -------------------------------------------------------------------------------- /src/contracts/protocol/protocol.nft/main.ts: -------------------------------------------------------------------------------- 1 | import { OutRef } from "@/types"; 2 | 3 | import { header, helios, HeliosScript, module } from "../../program"; 4 | 5 | export type Params = { 6 | protocolSeed: OutRef; 7 | }; 8 | 9 | export default function main({ protocolSeed }: Params): HeliosScript { 10 | return helios` 11 | ${header("minting", "nft__protocol")} 12 | 13 | import { 14 | PROTOCOL_PARAMS_NFT_TOKEN_NAME, 15 | PROTOCOL_PROPOSAL_NFT_TOKEN_NAME 16 | } from ${module("constants")} 17 | 18 | import { Redeemer } 19 | from ${module("nft__protocol__types")} 20 | 21 | const SEED_OUTPUT_ID: TxOutputId = 22 | TxOutputId::new( 23 | TxId::new(#${protocolSeed.txHash}), 24 | ${protocolSeed.outputIndex} 25 | ) 26 | 27 | func main(redeemer: Redeemer, ctx: ScriptContext) -> Bool { 28 | tx: Tx = ctx.tx; 29 | own_mph: MintingPolicyHash = ctx.get_current_minting_policy_hash(); 30 | 31 | redeemer.switch { 32 | 33 | Bootstrap => { 34 | assert( 35 | tx.minted.get_policy(own_mph).all( 36 | (token_name: ByteArray, amount: Int) -> { 37 | if (token_name == PROTOCOL_PARAMS_NFT_TOKEN_NAME) { amount == 1 } 38 | else if (token_name == PROTOCOL_PROPOSAL_NFT_TOKEN_NAME) { amount == 1 } 39 | else { false } 40 | } 41 | ), 42 | "Transaction must mint only two protocol params nft and protocol proposal nft" 43 | ); 44 | 45 | tx.inputs.any((input: TxInput) -> { input.output_id == SEED_OUTPUT_ID }) 46 | } 47 | 48 | } 49 | } 50 | `; 51 | } 52 | -------------------------------------------------------------------------------- /src/transactions/project/initiate-close.ts: -------------------------------------------------------------------------------- 1 | import { Lucid, UTxO } from "lucid-cardano"; 2 | 3 | import * as S from "@/schema"; 4 | import { ProjectDatum, ProjectRedeemer } from "@/schema/teiki/project"; 5 | import { TimeDifference } from "@/types"; 6 | import { assert } from "@/utils"; 7 | 8 | export type Params = { 9 | protocolParamsUtxo: UTxO; 10 | projectUtxo: UTxO; 11 | projectVRefScriptUtxo: UTxO; 12 | txValidUntil: TimeDifference; 13 | }; 14 | 15 | export function initiateCloseTx( 16 | lucid: Lucid, 17 | { 18 | protocolParamsUtxo, 19 | projectUtxo, 20 | projectVRefScriptUtxo, 21 | txValidUntil, 22 | }: Params 23 | ) { 24 | assert( 25 | projectVRefScriptUtxo.scriptRef != null, 26 | "Invalid project script UTxO: Missing reference script" 27 | ); 28 | 29 | assert( 30 | projectUtxo.datum != null, 31 | "Invalid project UTxO: Missing inline datum" 32 | ); 33 | const project = S.fromData(S.fromCbor(projectUtxo.datum), ProjectDatum); 34 | 35 | return lucid 36 | .newTx() 37 | .readFrom([protocolParamsUtxo, projectVRefScriptUtxo]) 38 | .collectFrom( 39 | [projectUtxo], 40 | S.toCbor(S.toData({ case: "InitiateClose" }, ProjectRedeemer)) 41 | ) 42 | .payToContract( 43 | projectUtxo.address, 44 | { 45 | inline: S.toCbor( 46 | S.toData( 47 | { 48 | ...project, 49 | status: { 50 | type: "PreClosed", 51 | pendingUntil: { timestamp: BigInt(txValidUntil) }, 52 | }, 53 | }, 54 | ProjectDatum 55 | ) 56 | ), 57 | }, 58 | projectUtxo.assets 59 | ) 60 | .validTo(txValidUntil); 61 | } 62 | -------------------------------------------------------------------------------- /src/schema/teiki/kolours.ts: -------------------------------------------------------------------------------- 1 | import { Static } from "@sinclair/typebox"; 2 | import { Address } from "lucid-cardano"; 3 | 4 | import { LovelaceAmount } from "@/types"; 5 | 6 | import { Enum, Void } from "../uplc"; 7 | 8 | export const KolourNftMintingRedeemer = Enum("case", { 9 | Mint: Void, 10 | Burn: Void, 11 | }); 12 | export type KolourNftMintingRedeemer = Static; 13 | 14 | export type Kolour = string; // RRGGBB 15 | 16 | export type Referral = { 17 | id: string; 18 | discount: number; 19 | }; 20 | 21 | export type KolourEntry = { 22 | fee: LovelaceAmount; 23 | listedFee: LovelaceAmount; 24 | image: string; // ipfs:// 25 | }; 26 | 27 | export type KolourQuotation = KolourQuotationProgram & { 28 | kolours: Record; 29 | baseDiscount: number; 30 | userAddress: Address; 31 | feeAddress: Address; 32 | expiration: number; // Unix Timestamp in seconds 33 | }; 34 | 35 | export type KolourQuotationProgram = 36 | | { source: { type: "present" }; referral?: undefined } 37 | | { source: { type: "free" }; referral?: undefined } 38 | | { 39 | source: { type: "genesis_kreation"; kreation: string }; 40 | referral?: Referral; 41 | }; 42 | 43 | export type KolourQuotationSource = KolourQuotationProgram["source"]; 44 | 45 | export type GenesisKreationId = string; // Act as token name also 46 | 47 | export type GenesisKreationQuotation = { 48 | id: GenesisKreationId; 49 | image: string; // ipfs:// 50 | fee: LovelaceAmount; 51 | listedFee: LovelaceAmount; 52 | baseDiscount: number; 53 | userAddress: Address; 54 | feeAddress: Address; 55 | referral?: Referral; 56 | expiration: number; // Unix Timestamp in seconds 57 | }; 58 | -------------------------------------------------------------------------------- /src/contracts/protocol/protocol-params.v/main.ts: -------------------------------------------------------------------------------- 1 | import { Hex } from "@/types"; 2 | 3 | import { header, helios, HeliosScript, module } from "../../program"; 4 | 5 | export type Params = { 6 | protocolNftMph: Hex; 7 | }; 8 | 9 | export default function main({ protocolNftMph }: Params): HeliosScript { 10 | return helios` 11 | ${header("spending", "v__protocol_params")} 12 | 13 | 14 | import { PROTOCOL_PROPOSAL_NFT_TOKEN_NAME } 15 | from ${module("constants")} 16 | 17 | import { Redeemer as ProposalRedeemer } 18 | from ${module("v__protocol_proposal__types")} 19 | 20 | import { Redeemer } 21 | from ${module("v__protocol_params__types")} 22 | 23 | const PROTOCOL_NFT_MPH: MintingPolicyHash = 24 | MintingPolicyHash::new(#${protocolNftMph}) 25 | 26 | const PROTOCOL_PROPOSAL_NFT_ASSET_CLASS: AssetClass = 27 | AssetClass::new(PROTOCOL_NFT_MPH, PROTOCOL_PROPOSAL_NFT_TOKEN_NAME) 28 | 29 | func main(_, redeemer: Redeemer, ctx: ScriptContext) -> Bool{ 30 | tx: Tx = ctx.tx; 31 | 32 | redeemer.switch { 33 | 34 | ApplyProposal => { 35 | proposal_txinput: TxInput = 36 | tx.inputs.find( 37 | (input: TxInput) -> { 38 | input.output.value.get_safe(PROTOCOL_PROPOSAL_NFT_ASSET_CLASS) == 1 39 | } 40 | ); 41 | 42 | proposal_purpose: ScriptPurpose = 43 | ScriptPurpose::new_spending(proposal_txinput.output_id); 44 | 45 | proposal_redeemer: Data = tx.redeemers.get(proposal_purpose); 46 | 47 | ProposalRedeemer::from_data(proposal_redeemer).switch { 48 | Apply => true, 49 | else => false 50 | } 51 | } 52 | 53 | } 54 | } 55 | `; 56 | } 57 | -------------------------------------------------------------------------------- /src/transactions/protocol/withdraw.ts: -------------------------------------------------------------------------------- 1 | import { Address, Data, Lucid, UTxO } from "lucid-cardano"; 2 | 3 | import * as S from "@/schema"; 4 | import { ProtocolParamsDatum } from "@/schema/teiki/protocol"; 5 | import { OpenTreasuryDatum } from "@/schema/teiki/treasury"; 6 | import { assert } from "@/utils"; 7 | 8 | import { RATIO_MULTIPLIER } from "../constants"; 9 | 10 | export type WithdrawProtocolRewardParams = { 11 | protocolParamsUtxo: UTxO; 12 | protocolStakeScriptRefUtxo: UTxO; 13 | rewards: bigint; 14 | stakeAddress: Address; 15 | openTreasuryAddress: Address; 16 | }; 17 | 18 | export function withdrawProtocolRewardTx( 19 | lucid: Lucid, 20 | { 21 | protocolParamsUtxo, 22 | protocolStakeScriptRefUtxo, 23 | rewards, 24 | stakeAddress, 25 | openTreasuryAddress, 26 | }: WithdrawProtocolRewardParams 27 | ) { 28 | assert(protocolParamsUtxo.datum != null, "Invalid protocol params UTxO"); 29 | 30 | const protocolParams = S.fromData( 31 | S.fromCbor(protocolParamsUtxo.datum), 32 | ProtocolParamsDatum 33 | ); 34 | 35 | const openTreasuryDatum: OpenTreasuryDatum = { 36 | governorAda: 37 | (rewards * protocolParams.governorShareRatio) / RATIO_MULTIPLIER, 38 | tag: { 39 | kind: "TagProtocolStakingRewards", 40 | stakingValidator: { 41 | script: protocolParams.registry.protocolStakingValidator.script, 42 | }, 43 | }, 44 | }; 45 | 46 | return lucid 47 | .newTx() 48 | .readFrom([protocolParamsUtxo, protocolStakeScriptRefUtxo]) 49 | .withdraw(stakeAddress, rewards, Data.void()) 50 | .payToContract( 51 | openTreasuryAddress, 52 | { inline: S.toCbor(S.toData(openTreasuryDatum, OpenTreasuryDatum)) }, 53 | { lovelace: rewards } 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/cli/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Lucid, Script, Address } from "lucid-cardano"; 3 | 4 | import { HeliosScript, helios, header } from "@/contracts/program"; 5 | import { signAndSubmit } from "@/helpers/lucid"; 6 | import { 7 | DeployScriptParams, 8 | deployScriptsTx, 9 | } from "@/transactions/deploy-scripts"; 10 | 11 | export async function deployReferencedScript( 12 | lucid: Lucid, 13 | scripts: Script[], 14 | referenceAddress: Address 15 | ) { 16 | console.log(`Deploying ${scripts.length} reference scripts ...`); 17 | 18 | const params: DeployScriptParams = { 19 | deployAddress: referenceAddress, 20 | scriptReferences: scripts, 21 | batchScriptSize: 15_000, 22 | }; 23 | 24 | const txs = deployScriptsTx(lucid, params); 25 | 26 | console.log("number of transactions :>> ", txs.length); 27 | 28 | for (const tx of txs) { 29 | await sleep(60_000); 30 | const txComplete = await tx.complete(); 31 | const txHash = await signAndSubmit(txComplete); 32 | const result = await lucid.awaitTx(txHash); 33 | console.log(result, txHash); 34 | } 35 | } 36 | 37 | export function alwaysFalse(): HeliosScript { 38 | return helios` 39 | ${header("spending", "v__locking")} 40 | 41 | func main() -> Bool { 42 | 731211 == 731112 43 | } 44 | `; 45 | } 46 | 47 | export function sleep(ms: number): Promise { 48 | return new Promise((resolve) => { 49 | setTimeout(resolve, ms); 50 | }); 51 | } 52 | 53 | export function printScriptHash(lucid: Lucid, scripts: any) { 54 | for (const key of Object.keys(scripts)) { 55 | const script: any = scripts[key as keyof typeof scripts]; 56 | console.log(`${key}=${lucid.utils.validatorToScriptHash(script)}`); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | parser: "@typescript-eslint/parser" 3 | extends: 4 | - eslint:recommended 5 | - plugin:@typescript-eslint/recommended 6 | - plugin:prettier/recommended 7 | - plugin:import/recommended 8 | - plugin:import/typescript 9 | plugins: 10 | - "@typescript-eslint" 11 | - prettier 12 | - import 13 | parserOptions: 14 | ecmaVersion: 2020 15 | project: 16 | - tsconfig.json 17 | sourceType: module 18 | overrides: 19 | - files: 20 | - "tests/**/*.test.+(js|ts)" 21 | plugins: 22 | - jest 23 | - jest-formatting 24 | extends: 25 | - plugin:jest/all 26 | - plugin:jest-formatting/recommended 27 | rules: 28 | jest/prefer-expect-assertions: 29 | - error 30 | - onlyFunctionsWithAsyncKeyword: true 31 | onlyFunctionsWithExpectInLoop: true 32 | onlyFunctionsWithExpectInCallback: true 33 | jest/require-top-level-describe: 34 | - "off" 35 | settings: 36 | import/parsers: 37 | "@typescript-eslint/parser": 38 | - .ts 39 | - .tsx 40 | import/resolver: 41 | typescript: true 42 | node: true 43 | rules: 44 | eol-last: 45 | - error 46 | - always 47 | "@typescript-eslint/no-unused-vars": 48 | - warn 49 | - varsIgnorePattern: "^_" 50 | argsIgnorePattern: "^_" 51 | caughtErrorsIgnorePattern: "^_" 52 | destructuredArrayIgnorePattern: "^_" 53 | import/order: 54 | - error 55 | - groups: 56 | - builtin 57 | - external 58 | - internal 59 | - parent 60 | - sibling 61 | - index 62 | - object 63 | - type 64 | - unknown 65 | newlines-between: always 66 | alphabetize: 67 | order: asc 68 | caseInsensitive: true 69 | # orderImportKind: asc 70 | -------------------------------------------------------------------------------- /src/json.ts: -------------------------------------------------------------------------------- 1 | // TODO: A duplication of teiki-web/modules/json-utils/index.ts 2 | import JsonBigFactory from "@shinka-network/json-bigint"; 3 | const JsonBig = JsonBigFactory({ useNativeBigInt: true }); 4 | 5 | export default JsonBig; 6 | 7 | /** 8 | * Converts a JSON-formatted string to a JS value. 9 | * 10 | * Big integers will be parsed as `bigint` values. 11 | * This function is the bigint-aware alternative of `JSON.parse`. 12 | */ 13 | export function fromJson( 14 | text: string, 15 | options?: { forceBigInt?: boolean } 16 | ): T { 17 | return options?.forceBigInt 18 | ? JsonBig.parse(text, (_, value) => 19 | Number.isInteger(value) ? BigInt(value) : value 20 | ) 21 | : JsonBig.parse(text); 22 | } 23 | 24 | type ValidJsonTypes = string | number | bigint | boolean | object | unknown[]; 25 | type InvalidJsonTypes = undefined | symbol | ((...args: unknown[]) => unknown); 26 | 27 | /** 28 | * Converts a JS value to a JSON-formatted string. 29 | * 30 | * Values of `bigint` will be stringified properly. 31 | * This function is the bigint-aware alternative of `JSON.stringify`. 32 | */ 33 | export function toJson( 34 | value: InvalidJsonTypes, 35 | replacer?: Replacer, 36 | space?: string | number 37 | ): undefined; 38 | export function toJson( 39 | value: ValidJsonTypes, 40 | replacer?: Replacer, 41 | space?: string | number 42 | ): string; 43 | export function toJson( 44 | value: unknown, 45 | replacer?: Replacer, 46 | space?: string | number 47 | ): string | undefined; 48 | export function toJson( 49 | value: unknown, 50 | replacer?: Replacer, 51 | space?: string | number 52 | ): string | undefined { 53 | return JsonBig.stringify(value, replacer, space); 54 | } 55 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 56 | type Replacer = (this: any, key: string, value: any) => any; 57 | -------------------------------------------------------------------------------- /src/contracts/backing/backing.v/main.ts: -------------------------------------------------------------------------------- 1 | import { Hex } from "@/types"; 2 | 3 | import { header, helios, module } from "../../program"; 4 | 5 | export type Params = { 6 | proofOfBackingMph: Hex; 7 | protocolNftMph: Hex; 8 | }; 9 | 10 | export default function main({ proofOfBackingMph, protocolNftMph }: Params) { 11 | return helios` 12 | ${header("spending", "v__backing")} 13 | 14 | import { find_pparams_datum_from_inputs } 15 | from ${module("helpers")} 16 | 17 | import { Datum as PParamsDatum } 18 | from ${module("v__protocol_params__types")} 19 | 20 | import { Redeemer as PobRedeemer } 21 | from ${module("mp__proof_of_backing__types")} 22 | 23 | import { Redeemer } 24 | from ${module("v__backing__types")} 25 | 26 | const PROTOCOL_NFT_MPH: MintingPolicyHash = 27 | MintingPolicyHash::new(#${protocolNftMph}) 28 | 29 | const PROOF_OF_BACKING_MPH: MintingPolicyHash = 30 | MintingPolicyHash::new(#${proofOfBackingMph}) 31 | 32 | func main(_, redeemer: Redeemer, ctx: ScriptContext) -> Bool { 33 | tx: Tx = ctx.tx; 34 | 35 | redeemer.switch { 36 | 37 | Unback => { 38 | pob_purpose: ScriptPurpose = ScriptPurpose::new_minting(PROOF_OF_BACKING_MPH); 39 | pob_redeemer: Data = tx.redeemers.get(pob_purpose); 40 | PobRedeemer::from_data(pob_redeemer).switch { 41 | Plant => true, 42 | else => false 43 | } 44 | }, 45 | 46 | Migrate => { 47 | pparams_datum: PParamsDatum = 48 | find_pparams_datum_from_inputs(tx.ref_inputs, PROTOCOL_NFT_MPH); 49 | own_validator_hash: ValidatorHash = ctx.get_current_validator_hash(); 50 | migration_asset_class: AssetClass = 51 | pparams_datum 52 | .registry 53 | .backing_validator 54 | .migrations 55 | .get(own_validator_hash); 56 | tx.minted.get_safe(migration_asset_class) != 0 57 | } 58 | 59 | } 60 | } 61 | `; 62 | } 63 | -------------------------------------------------------------------------------- /src/cli/meta-protocol/propose.ts: -------------------------------------------------------------------------------- 1 | import { Unit } from "lucid-cardano"; 2 | 3 | import { getLucid } from "@/commands/utils"; 4 | import { TEIKI_PLANT_NFT_TOKEN_NAME } from "@/contracts/common/constants"; 5 | import { signAndSubmit } from "@/helpers/lucid"; 6 | import * as S from "@/schema"; 7 | import { RulesProposal, TeikiPlantDatum } from "@/schema/teiki/meta-protocol"; 8 | import { proposeMetaProtocolProposalTx } from "@/transactions/meta-protocol/propose"; 9 | import { assert, trimToSlot } from "@/utils"; 10 | 11 | const lucid = await getLucid(); 12 | const teikiPlantNftMph = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; 13 | const proofOfBackingMph = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; 14 | const teikiPlantNftUnit: Unit = teikiPlantNftMph + TEIKI_PLANT_NFT_TOKEN_NAME; 15 | const teikiPlantAddress = "addr1xxxx"; 16 | 17 | const teikiPlantUtxo = ( 18 | await lucid.utxosAtWithUnit(teikiPlantAddress, teikiPlantNftUnit) 19 | )[0]; 20 | 21 | const teikiPlantScriptUtxo = ( 22 | await lucid.utxosByOutRef([{ txHash: "", outputIndex: 0 }]) 23 | )[0]; 24 | 25 | assert( 26 | teikiPlantUtxo.datum != null, 27 | "Invalid Teiki plant UTxO: Missing inline datum" 28 | ); 29 | const teikiPlantDatum = S.fromData( 30 | S.fromCbor(teikiPlantUtxo.datum), 31 | TeikiPlantDatum 32 | ); 33 | 34 | const proposedRules: RulesProposal = { 35 | inEffectAt: { timestamp: BigInt(Date.now() + 60000) }, 36 | rules: { 37 | ...teikiPlantDatum.rules, 38 | teikiMintingRules: [ 39 | { 40 | mintingPolicyHash: { script: { hash: proofOfBackingMph } }, 41 | redeemer: { kind: "ConstrNotIn", constrs: [2n] }, 42 | }, 43 | ], 44 | }, 45 | }; 46 | 47 | const txValidUntil = trimToSlot(Date.now()) + 600_000; 48 | 49 | const tx = proposeMetaProtocolProposalTx(lucid, { 50 | teikiPlantUtxo, 51 | teikiPlantScriptUtxo, 52 | proposedRules, 53 | txValidUntil, 54 | }); 55 | 56 | const txComplete = await tx.complete(); 57 | const txHash = await signAndSubmit(txComplete); 58 | 59 | await lucid.awaitTx(txHash); 60 | 61 | console.log("txHash :>> ", txHash); 62 | -------------------------------------------------------------------------------- /src/transactions/project/initiate-delist.ts: -------------------------------------------------------------------------------- 1 | import { Lucid, UTxO } from "lucid-cardano"; 2 | 3 | import * as S from "@/schema"; 4 | import { ProjectDatum, ProjectRedeemer } from "@/schema/teiki/project"; 5 | import { ProtocolParamsDatum } from "@/schema/teiki/protocol"; 6 | import { TimeDifference } from "@/types"; 7 | import { assert } from "@/utils"; 8 | 9 | export type InitiateDelistParams = { 10 | projectUtxos: UTxO[]; 11 | projectVRefScriptUtxo: UTxO; 12 | protocolParamsUtxo: UTxO; 13 | txValidUntil: TimeDifference; 14 | }; 15 | 16 | export function initiateDelistTx( 17 | lucid: Lucid, 18 | { 19 | projectUtxos, 20 | projectVRefScriptUtxo, 21 | protocolParamsUtxo, 22 | txValidUntil, 23 | }: InitiateDelistParams 24 | ) { 25 | assert( 26 | protocolParamsUtxo.datum != null, 27 | "Invalid protocol params UTxO: Missing inline datum" 28 | ); 29 | 30 | const protocolParams = S.fromData( 31 | S.fromCbor(protocolParamsUtxo.datum), 32 | ProtocolParamsDatum 33 | ); 34 | 35 | let tx = lucid 36 | .newTx() 37 | .readFrom([projectVRefScriptUtxo, protocolParamsUtxo]) 38 | .collectFrom( 39 | projectUtxos, 40 | S.toCbor(S.toData({ case: "InitiateDelist" }, ProjectRedeemer)) 41 | ) 42 | .validTo(txValidUntil); 43 | 44 | for (const projectUtxo of projectUtxos) { 45 | assert( 46 | projectUtxo.datum != null, 47 | "Invalid project UTxO: Missing inline datum" 48 | ); 49 | 50 | const projectDatum = S.fromData( 51 | S.fromCbor(projectUtxo.datum), 52 | ProjectDatum 53 | ); 54 | 55 | const outputProjectDatum: ProjectDatum = { 56 | ...projectDatum, 57 | status: { 58 | type: "PreDelisted", 59 | pendingUntil: { 60 | timestamp: 61 | BigInt(txValidUntil) + 62 | protocolParams.projectDelistWaitingPeriod.milliseconds, 63 | }, 64 | }, 65 | }; 66 | 67 | tx = tx.payToContract( 68 | projectUtxo.address, 69 | { inline: S.toCbor(S.toData(outputProjectDatum, ProjectDatum)) }, 70 | { ...projectUtxo.assets } 71 | ); 72 | } 73 | return tx; 74 | } 75 | -------------------------------------------------------------------------------- /src/transactions/protocol/propose.ts: -------------------------------------------------------------------------------- 1 | import { Lucid, UTxO } from "lucid-cardano"; 2 | 3 | import * as S from "@/schema"; 4 | import { 5 | ProtocolParamsDatum, 6 | ProtocolProposalDatum, 7 | ProtocolProposalRedeemer, 8 | } from "@/schema/teiki/protocol"; 9 | import { TimeDifference } from "@/types"; 10 | import { assert } from "@/utils"; 11 | 12 | import { 13 | constructTxOutputId, 14 | extractPaymentPubKeyHash, 15 | } from "../../helpers/schema"; 16 | 17 | export type ProposeProtocolTxParams = { 18 | protocolParamsUtxo: UTxO; 19 | proposedProtocolParamsDatum: ProtocolParamsDatum; 20 | protocolProposalUtxo: UTxO; 21 | protocolProposalRefScriptUtxo: UTxO; 22 | txValidUntil: TimeDifference; 23 | }; 24 | 25 | export function proposeProtocolProposalTx( 26 | lucid: Lucid, 27 | { 28 | protocolParamsUtxo, 29 | proposedProtocolParamsDatum, 30 | protocolProposalUtxo, 31 | protocolProposalRefScriptUtxo, 32 | txValidUntil, 33 | }: ProposeProtocolTxParams 34 | ) { 35 | assert(protocolParamsUtxo.datum, "Protocol params utxo must have datum"); 36 | const protocolParams = S.fromData( 37 | S.fromCbor(protocolParamsUtxo.datum), 38 | ProtocolParamsDatum 39 | ); 40 | 41 | const protocolGovernorPkh = extractPaymentPubKeyHash( 42 | protocolParams.governorAddress 43 | ); 44 | 45 | const protocolProposalDatum: ProtocolProposalDatum = { 46 | proposal: { 47 | inEffectAt: { 48 | timestamp: 49 | BigInt(txValidUntil) + 50 | protocolParams.proposalWaitingPeriod.milliseconds + 51 | 1n, 52 | }, 53 | base: constructTxOutputId(protocolParamsUtxo), 54 | params: proposedProtocolParamsDatum, 55 | }, 56 | }; 57 | 58 | return lucid 59 | .newTx() 60 | .addSignerKey(protocolGovernorPkh) 61 | .readFrom([protocolParamsUtxo, protocolProposalRefScriptUtxo]) 62 | .collectFrom( 63 | [protocolProposalUtxo], 64 | S.toCbor(S.toData({ case: "Propose" }, ProtocolProposalRedeemer)) 65 | ) 66 | .payToContract( 67 | protocolProposalUtxo.address, 68 | { 69 | inline: S.toCbor( 70 | S.toData(protocolProposalDatum, ProtocolProposalDatum) 71 | ), 72 | }, 73 | protocolProposalUtxo.assets 74 | ) 75 | .validTo(txValidUntil); 76 | } 77 | -------------------------------------------------------------------------------- /src/schema/teiki/treasury.ts: -------------------------------------------------------------------------------- 1 | import { StakingValidatorHash, Time } from "../helios"; 2 | import { Bool, Enum, Int, Map, Static, Struct, Void } from "../uplc"; 3 | 4 | import { ProjectId } from "./common"; 5 | import { TreasuryTag } from "./tags"; 6 | 7 | // ==================== V | Dedicated Treasury ==================== 8 | 9 | export const DedicatedTreasuryDatum = Struct({ 10 | projectId: ProjectId, 11 | governorAda: Int, 12 | tag: TreasuryTag, 13 | }); 14 | export type DedicatedTreasuryDatum = Static; 15 | 16 | export const DedicatedTreasuryRedeemer = Enum("case", { 17 | CollectFees: { fees: Int, split: Bool }, 18 | WithdrawAda: Void, 19 | Revoke: Void, 20 | Migrate: Void, 21 | }); 22 | export type DedicatedTreasuryRedeemer = Static< 23 | typeof DedicatedTreasuryRedeemer 24 | >; 25 | 26 | // ==================== V | Shared Treasury ==================== 27 | 28 | export const BurnAction = Enum("burn", { 29 | BurnPeriodically: Void, 30 | BurnEntirely: Void, 31 | }); 32 | export type BurnAction = Static; 33 | 34 | export const ProjectTeiki = Enum("teikiCondition", { 35 | TeikiEmpty: Void, 36 | TeikiBurntPeriodically: { 37 | available: Int, 38 | lastBurnAt: Time, 39 | }, 40 | TeikiBurntEntirely: Void, 41 | }); 42 | export type ProjectTeiki = Static; 43 | 44 | export const SharedTreasuryDatum = Struct({ 45 | projectId: ProjectId, 46 | governorTeiki: Int, 47 | projectTeiki: ProjectTeiki, 48 | tag: TreasuryTag, 49 | }); 50 | export type SharedTreasuryDatum = Static; 51 | 52 | export const SharedTreasuryRedeemer = Enum("case", { 53 | UpdateTeiki: { 54 | burnAction: BurnAction, 55 | burnAmount: Int, 56 | rewards: Int, 57 | }, 58 | Migrate: Void, 59 | }); 60 | export type SharedTreasuryRedeemer = Static; 61 | 62 | // ==================== V | Open Treasury ==================== 63 | 64 | export const OpenTreasuryDatum = Struct({ 65 | governorAda: Int, 66 | tag: TreasuryTag, 67 | }); 68 | export type OpenTreasuryDatum = Static; 69 | 70 | export const OpenTreasuryRedeemer = Enum("case", { 71 | CollectDelayedStakingRewards: { 72 | stakingWithdrawals: Map(StakingValidatorHash, Int), 73 | }, 74 | WithdrawAda: Void, 75 | Migrate: Void, 76 | }); 77 | export type OpenTreasuryRedeemer = Static; 78 | -------------------------------------------------------------------------------- /src/transactions/protocol/apply.ts: -------------------------------------------------------------------------------- 1 | import { Lucid, UTxO } from "lucid-cardano"; 2 | 3 | import * as S from "@/schema"; 4 | import { 5 | ProtocolParamsDatum, 6 | ProtocolParamsRedeemer, 7 | ProtocolProposalDatum, 8 | ProtocolProposalRedeemer, 9 | } from "@/schema/teiki/protocol"; 10 | import { UnixTime } from "@/types"; 11 | import { assert } from "@/utils"; 12 | 13 | import { extractPaymentPubKeyHash } from "../../helpers/schema"; 14 | 15 | export type ApplyProtocolTxParams = { 16 | protocolParamsUtxo: UTxO; 17 | protocolProposalUtxo: UTxO; 18 | protocolScriptUtxos: UTxO[]; 19 | txTime: UnixTime; 20 | }; 21 | 22 | export function applyProtocolProposalTx( 23 | lucid: Lucid, 24 | { 25 | protocolParamsUtxo, 26 | protocolProposalUtxo, 27 | protocolScriptUtxos, 28 | txTime, 29 | }: ApplyProtocolTxParams 30 | ) { 31 | assert(protocolParamsUtxo.datum, "Protocol params utxo must have datum"); 32 | const protocolParams = S.fromData( 33 | S.fromCbor(protocolParamsUtxo.datum), 34 | ProtocolParamsDatum 35 | ); 36 | 37 | assert(protocolProposalUtxo.datum, "Protocol proposal utxo must have datum"); 38 | const protocolProposalDatum = S.fromData( 39 | S.fromCbor(protocolProposalUtxo.datum), 40 | ProtocolProposalDatum 41 | ); 42 | 43 | assert(protocolProposalDatum.proposal, "Protocol proposal must not be empty"); 44 | const appliedProtocolParamsDatum = protocolProposalDatum.proposal.params; 45 | 46 | const protocolGovernorPkh = extractPaymentPubKeyHash( 47 | protocolParams.governorAddress 48 | ); 49 | 50 | return lucid 51 | .newTx() 52 | .addSignerKey(protocolGovernorPkh) 53 | .readFrom(protocolScriptUtxos) 54 | .collectFrom( 55 | [protocolParamsUtxo], 56 | S.toCbor(S.toData({ case: "ApplyProposal" }, ProtocolParamsRedeemer)) 57 | ) 58 | .collectFrom( 59 | [protocolProposalUtxo], 60 | S.toCbor(S.toData({ case: "Apply" }, ProtocolProposalRedeemer)) 61 | ) 62 | .payToContract( 63 | protocolProposalUtxo.address, 64 | { inline: S.toCbor(S.toData({ proposal: null }, ProtocolProposalDatum)) }, 65 | protocolProposalUtxo.assets 66 | ) 67 | .payToContract( 68 | protocolParamsUtxo.address, 69 | { 70 | inline: S.toCbor( 71 | S.toData(appliedProtocolParamsDatum, ProtocolParamsDatum) 72 | ), 73 | }, 74 | protocolParamsUtxo.assets 75 | ) 76 | .validFrom(txTime); 77 | } 78 | -------------------------------------------------------------------------------- /src/schema/teiki/meta-protocol.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Duration, 3 | MintingPolicyHash, 4 | PaymentCredential, 5 | Time, 6 | } from "../helios"; 7 | import { 8 | ByteArray, 9 | Enum, 10 | Int, 11 | List, 12 | Option, 13 | Static, 14 | Struct, 15 | Void, 16 | } from "../uplc"; 17 | 18 | // ==================== Predicates ==================== 19 | 20 | export const MintingRedeemer = Enum("kind", { 21 | Any: Void, 22 | ConstrIn: { constrs: List(Int) }, 23 | ConstrNotIn: { constrs: List(Int) }, 24 | }); 25 | export type MintingRedeemer = Static; 26 | 27 | export const MintingPredicate = Struct({ 28 | mintingPolicyHash: MintingPolicyHash, 29 | redeemer: MintingRedeemer, 30 | }); 31 | export type MintingPredicate = Static; 32 | 33 | export const TokenPredicate = Struct({ 34 | mintingPolicyHash: MintingPolicyHash, 35 | tokenNames: Option(List(ByteArray)), 36 | }); 37 | export type TokenPredicate = Static; 38 | 39 | // ==================== V | Teiki Plant ==================== 40 | 41 | export const Authorization = Enum("authorization", { 42 | MustBe: { credential: PaymentCredential }, 43 | MustHave: { predicate: TokenPredicate }, 44 | MustMint: { predicate: MintingPredicate }, 45 | }); 46 | export type Authorization = Static; 47 | 48 | export const Rules = Struct({ 49 | teikiMintingRules: List(MintingPredicate), 50 | proposalAuthorizations: List(Authorization), 51 | proposalWaitingPeriod: Duration, 52 | }); 53 | export type Rules = Static; 54 | 55 | export const RulesProposal = Struct({ 56 | inEffectAt: Time, 57 | rules: Rules, 58 | }); 59 | export type RulesProposal = Static; 60 | 61 | export const TeikiPlantDatum = Struct({ 62 | rules: Rules, 63 | proposal: Option(RulesProposal), 64 | }); 65 | export type TeikiPlantDatum = Static; 66 | 67 | export const TeikiPlantRedeemer = Enum("case", { 68 | Propose: Void, 69 | Apply: Void, 70 | Cancel: Void, 71 | }); 72 | export type TeikiPlantRedeemer = Static; 73 | 74 | // ==================== NFT | Teiki Plant ==================== 75 | 76 | export const TeikiPlantNftMintingRedeemer = Enum("case", { Bootstrap: Void }); 77 | export type TeikiPlantNftMintingRedeemer = Static< 78 | typeof TeikiPlantNftMintingRedeemer 79 | >; 80 | 81 | // ==================== MP | Teiki ==================== 82 | 83 | export const TeikiMintingRedeemer = Enum("case", { 84 | Mint: Void, 85 | Burn: Void, 86 | Evolve: Void, 87 | }); 88 | export type TeikiMintingRedeemer = Static; 89 | -------------------------------------------------------------------------------- /src/helpers/lucid.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Lucid, 3 | ScriptHash, 4 | Address, 5 | AddressDetails, 6 | getAddressDetails, 7 | TxComplete, 8 | Core, 9 | fromHex, 10 | } from "lucid-cardano"; 11 | 12 | import { Hex } from "@/types"; 13 | import { assert } from "@/utils"; 14 | 15 | export async function signAndSubmit(tx: TxComplete): Promise { 16 | const signedTx = await tx.sign().complete(); 17 | const txId = await signedTx.submit(); 18 | return txId; 19 | } 20 | 21 | export function getPaymentKeyHash(address: Address): Hex { 22 | const { paymentCredential } = getAddressDetails(address); 23 | assert(paymentCredential, "Cannot extract payment credential from address"); 24 | assert(paymentCredential.type === "Key", "Not a key hash payment credential"); 25 | return paymentCredential.hash; 26 | } 27 | 28 | export function getUserAddressKeyHashes(address: Address) { 29 | const { paymentCredential, stakeCredential } = getAddressDetails(address); 30 | assert( 31 | paymentCredential && paymentCredential.type === "Key", 32 | "Cannot extract payment key hash from address" 33 | ); 34 | return { 35 | paymentKeyHash: paymentCredential.hash, 36 | stakeKeyHash: 37 | stakeCredential && stakeCredential.type === "Key" 38 | ? stakeCredential.hash 39 | : null, 40 | }; 41 | } 42 | 43 | export function getAddressDetailsSafe(address: Address): AddressDetails | null { 44 | try { 45 | return getAddressDetails(address); 46 | } catch (e) { 47 | if (e instanceof Error && e.message.includes("No address type matched for")) 48 | return null; 49 | throw e; 50 | } 51 | } 52 | 53 | export function addressFromScriptHashes( 54 | lucid: Lucid, 55 | paymentScriptHash: ScriptHash, 56 | stakeScriptHash?: ScriptHash 57 | ): Address { 58 | return stakeScriptHash 59 | ? lucid.utils.credentialToAddress( 60 | lucid.utils.scriptHashToCredential(paymentScriptHash), 61 | lucid.utils.scriptHashToCredential(stakeScriptHash) 62 | ) 63 | : lucid.utils.credentialToAddress( 64 | lucid.utils.scriptHashToCredential(paymentScriptHash) 65 | ); 66 | } 67 | 68 | // https://github.com/spacebudz/lucid/blob/2d73e7d71d180c3aab7db654f3558279efb5dbb5/src/provider/emulator.ts#L280 69 | export function extractWitnessKeyHashes({ 70 | txId, 71 | witnesses, 72 | }: { 73 | txId: Hex; 74 | witnesses: Core.TransactionWitnessSet; 75 | }) { 76 | const vkeys = witnesses.vkeys(); 77 | if (!vkeys) return []; 78 | const keyHashes = []; 79 | for (let i = 0, n = vkeys.len(); i < n; i++) { 80 | const witness = vkeys.get(i); 81 | const publicKey = witness.vkey().public_key(); 82 | const keyHash = publicKey.hash().to_hex(); 83 | if (!publicKey.verify(fromHex(txId), witness.signature())) 84 | throw new Error(`Invalid vkey witness. Key hash: ${keyHash}`); 85 | keyHashes.push(keyHash); 86 | } 87 | return keyHashes; 88 | } 89 | -------------------------------------------------------------------------------- /src/contracts/meta-protocol/teiki.mp/main.ts: -------------------------------------------------------------------------------- 1 | import { Hex } from "@/types"; 2 | 3 | import { HeliosScript, helios, header, module } from "../../program"; 4 | 5 | export type Params = { 6 | teikiPlantNftMph: Hex; 7 | }; 8 | 9 | export default function main({ teikiPlantNftMph }: Params): HeliosScript { 10 | return helios` 11 | ${header("minting", "mp__teiki")} 12 | 13 | import { 14 | TEIKI_TOKEN_NAME, 15 | TEIKI_PLANT_NFT_TOKEN_NAME 16 | } from ${module("constants")} 17 | 18 | import { 19 | does_tx_pass_minting_preciate_check 20 | } from ${module("helpers")} 21 | 22 | import { 23 | Datum as TeikiPlantDatum, 24 | MintingPredicate 25 | } from ${module("v__teiki_plant__types")} 26 | 27 | import { Redeemer } 28 | from ${module("mp__teiki__types")} 29 | 30 | const TEIKI_PLANT_NFT_ASSET_CLASS: AssetClass = 31 | AssetClass::new( 32 | MintingPolicyHash::new(#${teikiPlantNftMph}), 33 | TEIKI_PLANT_NFT_TOKEN_NAME 34 | ) 35 | 36 | func main(redeemer: Redeemer, ctx: ScriptContext) -> Bool { 37 | tx: Tx = ctx.tx; 38 | own_mph: MintingPolicyHash = ctx.get_current_minting_policy_hash(); 39 | 40 | redeemer.switch { 41 | 42 | Mint => { 43 | teiki_plant_output: TxOutput = 44 | tx.ref_inputs 45 | .find( 46 | (input: TxInput) -> { 47 | input.output.value.get_safe(TEIKI_PLANT_NFT_ASSET_CLASS) == 1 48 | } 49 | ) 50 | .output; 51 | 52 | teiki_plant_datum: TeikiPlantDatum = 53 | teiki_plant_output.datum.switch { 54 | i: Inline => TeikiPlantDatum::from_data(i.data), 55 | else => error("Invalid teiki-plant UTxO: missing inline datum") 56 | }; 57 | 58 | is_minting_rules_passed: Bool = teiki_plant_datum.rules.teiki_minting_rules.any( 59 | (teiki_minting_predicate: MintingPredicate) -> { 60 | does_tx_pass_minting_preciate_check(tx, teiki_minting_predicate) 61 | } 62 | ); 63 | 64 | is_only_teiki_minted: Bool = tx.minted.get_policy(own_mph).all( 65 | (token_name: ByteArray, _) -> { 66 | token_name == TEIKI_TOKEN_NAME 67 | } 68 | ); 69 | 70 | is_minting_rules_passed && is_only_teiki_minted 71 | }, 72 | 73 | Burn => { 74 | tx.minted.get_policy(own_mph).all( 75 | (token_name: ByteArray, amount: Int) -> { 76 | if (token_name == TEIKI_TOKEN_NAME) { amount < 0 } 77 | else { false } 78 | } 79 | ) 80 | }, 81 | 82 | Evolve => { 83 | !tx.outputs.any( 84 | (output: TxOutput) -> { 85 | output.value.contains_policy(own_mph) 86 | } 87 | ) 88 | } 89 | 90 | } 91 | } 92 | `; 93 | } 94 | -------------------------------------------------------------------------------- /src/cli/protocol/propose.ts: -------------------------------------------------------------------------------- 1 | import { Unit } from "lucid-cardano"; 2 | 3 | import { 4 | SAMPLE_PROTOCOL_NON_SCRIPT_PARAMS, 5 | getProtocolRegistry, 6 | } from "@/commands/generate-protocol-params"; 7 | import { getLucid } from "@/commands/utils"; 8 | import { PROTOCOL_NFT_TOKEN_NAMES } from "@/contracts/common/constants"; 9 | import { signAndSubmit } from "@/helpers/lucid"; 10 | import { constructAddress } from "@/helpers/schema"; 11 | import { ProtocolParamsDatum, Registry } from "@/schema/teiki/protocol"; 12 | import { proposeProtocolProposalTx } from "@/transactions/protocol/propose"; 13 | import { trimToSlot } from "@/utils"; 14 | 15 | const lucid = await getLucid(); 16 | const governorAddress = await lucid.wallet.address(); 17 | 18 | const currentProtocolNftMph = 19 | "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; 20 | const currentTeikiPlantNftMph = 21 | "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; 22 | 23 | const proposalNftUnit: Unit = 24 | currentProtocolNftMph + PROTOCOL_NFT_TOKEN_NAMES.PROPOSAL; 25 | const protocolProposalVAddress = "addr1xxxx"; 26 | 27 | const protocolParamsUtxo = ( 28 | await lucid.utxosByOutRef([ 29 | { 30 | txHash: "", 31 | outputIndex: 0, 32 | }, 33 | ]) 34 | )[0]; 35 | 36 | const protocolProposalUtxo = ( 37 | await lucid.utxosAtWithUnit(protocolProposalVAddress, proposalNftUnit) 38 | )[0]; 39 | 40 | const protocolProposalRefScriptUtxo = ( 41 | await lucid.utxosByOutRef([ 42 | { 43 | txHash: "", 44 | outputIndex: 0, 45 | }, 46 | ]) 47 | )[0]; 48 | 49 | const proposeMigrateTokenMph = ""; 50 | const proposeMigrateTokenName = ""; 51 | 52 | // NOTE: only need to attach the migration info to the current registry 53 | const proposedRegistry: Registry = getProtocolRegistry(lucid, { 54 | protocolNftMph: currentProtocolNftMph, 55 | teikiPlantNftMph: currentTeikiPlantNftMph, 56 | migrationInfo: { 57 | migrateTokenMph: proposeMigrateTokenMph, 58 | migrateTokenName: proposeMigrateTokenName, 59 | }, 60 | }); 61 | 62 | const proposedNonScriptParams = SAMPLE_PROTOCOL_NON_SCRIPT_PARAMS; 63 | 64 | const proposedGovernorAddress = governorAddress; 65 | const proposedStakingManagerAddress = governorAddress; 66 | 67 | const proposedProtocolParamsDatum: ProtocolParamsDatum = { 68 | registry: proposedRegistry, 69 | governorAddress: constructAddress(proposedGovernorAddress), 70 | stakingManager: constructAddress(proposedStakingManagerAddress) 71 | .paymentCredential, 72 | ...proposedNonScriptParams, 73 | }; 74 | 75 | const txValidUntil = trimToSlot(Date.now()) + 600_000; 76 | 77 | const tx = proposeProtocolProposalTx(lucid, { 78 | protocolParamsUtxo, 79 | proposedProtocolParamsDatum, 80 | protocolProposalUtxo, 81 | protocolProposalRefScriptUtxo, 82 | txValidUntil, 83 | }); 84 | 85 | const txComplete = await tx.complete(); 86 | const txHash = await signAndSubmit(txComplete); 87 | 88 | await lucid.awaitTx(txHash); 89 | 90 | console.log("txHash :>> ", txHash); 91 | -------------------------------------------------------------------------------- /src/schema/teiki/project.ts: -------------------------------------------------------------------------------- 1 | import { Address, StakingValidatorHash, Time, TxOutputId } from "../helios"; 2 | import { Bool, Enum, Int, Option, Static, Struct, Void } from "../uplc"; 3 | 4 | import { IpfsCid, ProjectId } from "./common"; 5 | 6 | // ==================== V | Project ==================== 7 | 8 | export const ProjectStatus = Enum("type", { 9 | Active: Void, 10 | PreClosed: { pendingUntil: Time }, 11 | PreDelisted: { pendingUntil: Time }, 12 | Closed: { closedAt: Time }, 13 | Delisted: { delistedAt: Time }, 14 | }); 15 | export type ProjectStatus = Static; 16 | 17 | export const ProjectDatum = Struct({ 18 | projectId: ProjectId, 19 | ownerAddress: Address, 20 | status: ProjectStatus, 21 | milestoneReached: Int, 22 | isStakingDelegationManagedByProtocol: Bool, 23 | }); 24 | export type ProjectDatum = Static; 25 | 26 | export const ProjectRedeemer = Enum("case", { 27 | RecordNewMilestone: { newMilestone: Int }, 28 | AllocateStakingValidator: { newStakingValidator: StakingValidatorHash }, 29 | UpdateStakingDelegationManagement: Void, 30 | InitiateClose: Void, 31 | FinalizeClose: Void, 32 | InitiateDelist: Void, 33 | CancelDelist: Void, 34 | FinalizeDelist: Void, 35 | Migrate: Void, 36 | }); 37 | export type ProjectRedeemer = Static; 38 | 39 | // ==================== V | Project Detail ==================== 40 | 41 | export const Sponsorship = Struct({ 42 | amount: Int, 43 | until: Time, 44 | }); 45 | export type Sponsorship = Static; 46 | 47 | export const ProjectDetailDatum = Struct({ 48 | projectId: ProjectId, 49 | withdrawnFunds: Int, 50 | sponsorship: Option(Sponsorship), 51 | informationCid: IpfsCid, 52 | lastAnnouncementCid: Option(IpfsCid), 53 | }); 54 | export type ProjectDetailDatum = Static; 55 | 56 | export const ProjectDetailRedeemer = Enum("case", { 57 | WithdrawFunds: Void, 58 | Update: Void, 59 | Close: Void, 60 | Delist: Void, 61 | Migrate: Void, 62 | }); 63 | export type ProjectDetailRedeemer = Static; 64 | 65 | // ==================== V | Project Script ==================== 66 | 67 | export const ProjectScriptDatum = Struct({ 68 | projectId: ProjectId, 69 | stakingKeyDeposit: Int, 70 | }); 71 | export type ProjectScriptDatum = Static; 72 | 73 | export const ProjectScriptRedeemer = Enum("case", { 74 | Close: Void, 75 | Delist: Void, 76 | Migrate: Void, 77 | }); 78 | export type ProjectScriptRedeemer = Static; 79 | 80 | // ==================== AT | Project ==================== 81 | 82 | export const ProjectMintingRedeemer = Enum("case", { 83 | NewProject: { projectSeed: TxOutputId }, 84 | AllocateStaking: Void, 85 | DeallocateStaking: Void, 86 | MigrateOut: Void, 87 | MigrateIn: Void, 88 | }); 89 | export type ProjectMintingRedeemer = Static; 90 | -------------------------------------------------------------------------------- /src/transactions/protocol/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | Data, 4 | Lucid, 5 | PolicyId, 6 | PoolId, 7 | Script, 8 | Unit, 9 | UTxO, 10 | } from "lucid-cardano"; 11 | 12 | import { PROTOCOL_NFT_TOKEN_NAMES } from "@/contracts/common/constants"; 13 | import { getPaymentKeyHash } from "@/helpers/lucid"; 14 | import * as S from "@/schema"; 15 | import { 16 | ProtocolNonScriptParams, 17 | ProtocolParamsDatum, 18 | Registry, 19 | } from "@/schema/teiki/protocol"; 20 | import { TimeDifference, UnixTime } from "@/types"; 21 | 22 | import { constructAddress } from "../../helpers/schema"; 23 | 24 | export type BootstrapProtocolParams = { 25 | protocolParams: ProtocolNonScriptParams; 26 | seedUtxo: UTxO; 27 | governorAddress: Address; 28 | stakingManagerAddress: Address; 29 | poolId: PoolId; 30 | registry: Registry; 31 | protocolNftScript: Script; 32 | protocolParamsAddress: Address; 33 | protocolProposalAddress: Address; 34 | protocolStakeAddress: Address; 35 | protocolStakeValidator: Script; 36 | txTime: UnixTime; 37 | txTtl?: TimeDifference; 38 | }; 39 | 40 | export function bootstrapProtocolTx( 41 | lucid: Lucid, 42 | { 43 | protocolParams, 44 | seedUtxo, 45 | governorAddress, 46 | stakingManagerAddress, 47 | poolId, 48 | registry, 49 | protocolNftScript, 50 | protocolParamsAddress, 51 | protocolProposalAddress, 52 | protocolStakeValidator, 53 | protocolStakeAddress, 54 | txTime, 55 | txTtl = 600_000, 56 | }: BootstrapProtocolParams 57 | ) { 58 | const protocolParamsDatum: ProtocolParamsDatum = { 59 | registry, 60 | governorAddress: constructAddress(governorAddress), 61 | stakingManager: constructAddress(stakingManagerAddress).paymentCredential, 62 | ...protocolParams, 63 | }; 64 | 65 | const protocolNftPolicyId: PolicyId = 66 | lucid.utils.mintingPolicyToId(protocolNftScript); 67 | 68 | const paramsNftUnit: Unit = 69 | protocolNftPolicyId + PROTOCOL_NFT_TOKEN_NAMES.PARAMS; 70 | const proposalNftUnit: Unit = 71 | protocolNftPolicyId + PROTOCOL_NFT_TOKEN_NAMES.PROPOSAL; 72 | 73 | return lucid 74 | .newTx() 75 | .addSignerKey(getPaymentKeyHash(governorAddress)) 76 | .collectFrom([seedUtxo]) 77 | .mintAssets( 78 | { 79 | [paramsNftUnit]: 1n, 80 | [proposalNftUnit]: 1n, 81 | }, 82 | Data.void() 83 | ) 84 | .attachMintingPolicy(protocolNftScript) 85 | .payToContract( 86 | protocolParamsAddress, 87 | { inline: S.toCbor(S.toData(protocolParamsDatum, ProtocolParamsDatum)) }, 88 | { [paramsNftUnit]: 1n } 89 | ) 90 | .payToContract( 91 | protocolProposalAddress, 92 | { inline: Data.void() }, 93 | { [proposalNftUnit]: 1n } 94 | ) 95 | .registerStake(protocolStakeAddress) 96 | .delegateTo(protocolStakeAddress, poolId, Data.void()) 97 | .attachCertificateValidator(protocolStakeValidator) 98 | .validFrom(txTime) 99 | .validTo(txTime + txTtl); 100 | } 101 | -------------------------------------------------------------------------------- /src/schema/teiki/protocol.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | AssetClass, 4 | Duration, 5 | PaymentCredential, 6 | ScriptHash, 7 | Time, 8 | TxOutputId, 9 | ValidatorHash, 10 | } from "../helios"; 11 | import { Enum, Int, List, Map, Option, Static, Struct, Void } from "../uplc"; 12 | 13 | // ==================== V | Protocol Params ==================== 14 | 15 | export const MigratableScript = Struct({ 16 | latest: ValidatorHash, 17 | migrations: Map(ValidatorHash, AssetClass), 18 | }); 19 | export type MigratableScript = Static; 20 | 21 | export const Registry = Struct({ 22 | protocolStakingValidator: ScriptHash, 23 | projectValidator: MigratableScript, 24 | projectDetailValidator: MigratableScript, 25 | projectScriptValidator: MigratableScript, 26 | backingValidator: MigratableScript, 27 | dedicatedTreasuryValidator: MigratableScript, 28 | sharedTreasuryValidator: MigratableScript, 29 | openTreasuryValidator: MigratableScript, 30 | }); 31 | export type Registry = Static; 32 | 33 | export const ProtocolParamsDatum = Struct({ 34 | registry: Registry, 35 | governorAddress: Address, 36 | governorShareRatio: Int, 37 | protocolFundsShareRatio: Int, 38 | discountCentPrice: Int, 39 | projectMilestones: List(Int), 40 | teikiCoefficient: Int, 41 | projectTeikiBurnRate: Int, 42 | epochLength: Duration, 43 | projectPledge: Int, 44 | stakingManager: PaymentCredential, 45 | projectCreationFee: Int, 46 | projectSponsorshipMinFee: Int, 47 | projectSponsorshipDuration: Duration, 48 | projectInformationUpdateFee: Int, 49 | projectAnnouncementFee: Int, 50 | minTreasuryPerMilestoneEvent: Int, 51 | stakeKeyDeposit: Int, 52 | proposalWaitingPeriod: Duration, 53 | projectDelistWaitingPeriod: Duration, 54 | }); 55 | export type ProtocolParamsDatum = Static; 56 | export type ProtocolNonScriptParams = Omit< 57 | ProtocolParamsDatum, 58 | "registry" | "governorAddress" | "stakingManager" 59 | >; 60 | 61 | export const ProtocolParamsRedeemer = Enum("case", { 62 | ApplyProposal: Void, 63 | }); 64 | export type ProtocolParamsRedeemer = Static; 65 | 66 | // ==================== V | Protocol Proposal ==================== 67 | 68 | export const ProtocolProposal = Struct({ 69 | inEffectAt: Time, 70 | base: TxOutputId, 71 | params: ProtocolParamsDatum, 72 | }); 73 | export type ProtocolProposal = Static; 74 | 75 | export const ProtocolProposalDatum = Struct({ 76 | proposal: Option(ProtocolProposal), 77 | }); 78 | export type ProtocolProposalDatum = Static; 79 | 80 | export const ProtocolProposalRedeemer = Enum("case", { 81 | Propose: Void, 82 | Apply: Void, 83 | Cancel: Void, 84 | }); 85 | export type ProtocolProposalRedeemer = Static; 86 | 87 | // ==================== NFT | Protocol ==================== 88 | 89 | export const ProtocolNftMintingRedeemer = Enum("case", { 90 | Bootstrap: Void, 91 | }); 92 | export type ProtocolNftMintingRedeemer = Static< 93 | typeof ProtocolNftMintingRedeemer 94 | >; 95 | -------------------------------------------------------------------------------- /src/contracts/program.ts: -------------------------------------------------------------------------------- 1 | import { Program } from "@hyperionbt/helios"; 2 | 3 | import { assert } from "@/utils"; 4 | 5 | export type HeliosScriptPurpose = 6 | | "spending" 7 | | "minting" 8 | | "staking" 9 | | "testing" 10 | | "module"; 11 | 12 | export type HeliosScript = { 13 | purpose: HeliosScriptPurpose; 14 | name: string; 15 | source: string; 16 | dependencies: string[]; 17 | simplify?: boolean; 18 | }; 19 | 20 | const HeliosHeaderToken = Symbol.for("HeliosHeaderToken"); 21 | export function header(purpose: HeliosScriptPurpose, name: string) { 22 | return { __tag__: HeliosHeaderToken, purpose, name } as const; 23 | } 24 | 25 | const HeliosModuleToken = Symbol.for("HeliosModuleToken"); 26 | export function module(name: string) { 27 | return { __tag__: HeliosModuleToken, name } as const; 28 | } 29 | 30 | type HeliosTokens = 31 | | ReturnType 32 | | ReturnType 33 | | boolean 34 | | string 35 | | number 36 | | bigint; 37 | 38 | export type HeliosModules = Record; 39 | 40 | export function helios( 41 | strings: TemplateStringsArray, 42 | ...tokens: HeliosTokens[] 43 | ): HeliosScript { 44 | let purpose: HeliosScriptPurpose | undefined = undefined; 45 | let name: string | undefined = undefined; 46 | const dependencies: string[] = []; 47 | const values = []; 48 | 49 | for (const token of tokens) { 50 | assert(token != null, "null and undefined are not allowed"); 51 | if (typeof token === "object") { 52 | assert("__tag__" in token, "objects are not allowed"); 53 | switch (token.__tag__) { 54 | case HeliosHeaderToken: 55 | purpose = token.purpose; 56 | name = token.name; 57 | values.push(`${purpose} ${name}`); 58 | break; 59 | case HeliosModuleToken: 60 | dependencies.push(token.name); 61 | values.push(token.name); 62 | break; 63 | } 64 | } else if (typeof token === "boolean") { 65 | values.push(token ? "true" : "false"); 66 | } else { 67 | values.push(token); 68 | } 69 | } 70 | assert(purpose && name, "Script purpose and name must be set"); 71 | return { 72 | purpose, 73 | name, 74 | source: String.raw({ raw: strings }, ...values), 75 | dependencies, 76 | }; 77 | } 78 | 79 | export function newProgram( 80 | main: HeliosScript, 81 | modules: HeliosModules 82 | ): Program { 83 | const modSrcs: string[] = []; 84 | const visited = new Set(); 85 | function dfs(mod: HeliosScript) { 86 | visited.add(mod.name); 87 | if (mod !== main) modSrcs.push(mod.source); 88 | for (const dep of mod.dependencies) 89 | if (!visited.has(dep)) { 90 | const depMod = modules[dep]; 91 | assert(depMod, `Module ${dep} not found!`); 92 | dfs(depMod); 93 | } 94 | } 95 | dfs(main); 96 | return Program.new(main.source, modSrcs); 97 | } 98 | 99 | export function loadModules(modules: HeliosScript[]): HeliosModules { 100 | return Object.fromEntries(modules.map((mod) => [mod.name, mod])); 101 | } 102 | -------------------------------------------------------------------------------- /src/schema/helios.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ByteArray, 3 | ConStruct, 4 | Enum, 5 | Inline, 6 | Int, 7 | Map, 8 | Option, 9 | Struct, 10 | Id, 11 | type Static, 12 | } from "./uplc"; 13 | 14 | // Helios builtins 15 | export const Hash = Id("Hash")(Struct({ hash: ByteArray })); 16 | 17 | const IScriptHash = Struct({ script: Hash }); 18 | export const ScriptHash = Id("ScriptHash")(IScriptHash); 19 | export const ValidatorHash = Id("ValidatorHash")(IScriptHash); 20 | export const MintingPolicyHash = Id("MintingPolicyHash")(IScriptHash); 21 | export const StakingValidatorHash = Id("StakingValidatorHash")(IScriptHash); 22 | 23 | const IKeyHash = Struct({ key: Hash }); 24 | export const PubKeyHash = Id("PubKeyHash")(IKeyHash); 25 | export const StakeKeyHash = Id("StakeKeyHash")(IKeyHash); 26 | 27 | export const PaymentCredential = Id("PaymentCredential")( 28 | Enum("type", { 29 | PubKey: Inline(PubKeyHash, "hash"), 30 | Validator: Inline(ValidatorHash, "hash"), 31 | }) 32 | ); 33 | export const StakingCredential = Id("StakingCredential")( 34 | Enum("kind", { 35 | Hash: Inline( 36 | Enum("type", { 37 | StakeKey: Inline(StakeKeyHash, "hash"), 38 | Validator: Inline(ValidatorHash, "hash"), 39 | }) 40 | ), 41 | Ptr: { slotNo: Int, txIndex: Int, certIndex: Int }, 42 | }) 43 | ); 44 | export const Address = Id("Address")( 45 | ConStruct({ 46 | paymentCredential: PaymentCredential, 47 | stakingCredential: Option(StakingCredential), 48 | }) 49 | ); 50 | 51 | export const TxId = Id("TxId")(ConStruct(Inline(ByteArray))); 52 | export const TxOutputId = Id("TxOutputId")( 53 | ConStruct({ txId: TxId, index: Int }) 54 | ); 55 | 56 | export const AssetClass = Id("AssetClass")( 57 | ConStruct({ 58 | mintingPolicyHash: MintingPolicyHash, 59 | tokenName: ByteArray, 60 | }) 61 | ); 62 | export const Value = Id("Value")( 63 | Struct(Inline(Map(MintingPolicyHash, Map(ByteArray, Int)))) 64 | ); 65 | 66 | export const Time = Id("Time")(Struct({ timestamp: Int })); 67 | export const Duration = Id("Duration")(Struct({ milliseconds: Int })); 68 | 69 | export type Hash = Static; 70 | export type ScriptHash = Static; 71 | export type ValidatorHash = Static; 72 | export type MintingPolicyHash = Static; 73 | export type StakingValidatorHash = Static; 74 | export type PubKeyHash = Static; 75 | export type StakeKeyHash = Static; 76 | export type PaymentCredential = Static; 77 | export type StakingCredential = Static; 78 | export type Address = Static; 79 | export type TxId = Static; 80 | export type TxOutputId = Static; 81 | export type AssetClass = Static; 82 | export type Value = Static; 83 | export type Time = Static; 84 | export type Duration = Static; 85 | 86 | // Missing types 87 | // - DCert 88 | // - DatumHash 89 | // - OutputDatum 90 | // - PubKey 91 | // - ScriptContext 92 | // - ScriptPurpose 93 | // - StakingPurpose 94 | // - TimeRange 95 | // - Tx 96 | // - TxInput 97 | // - TxOutput 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kreate/protocol", 3 | "version": "0.1.0", 4 | "description": "Kreate Protocol implementation in Generation I", 5 | "keywords": [ 6 | "Membership" 7 | ], 8 | "homepage": "https://github.com/kreate-community/kreate-protocol#readme", 9 | "bugs": { 10 | "url": "https://github.com/kreate-community/kreate-protocol/issues" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/kreate-community/kreate-protocol.git" 15 | }, 16 | "license": "GPL-3.0", 17 | "author": "Shinka Network", 18 | "type": "module", 19 | "exports": { 20 | "./commands/*": "./dist/commands/*.js", 21 | "./contracts/*": "./dist/contracts/*.js", 22 | "./helpers/*": "./dist/helpers/*.js", 23 | "./json": "./dist/json.js", 24 | "./schema": "./dist/schema/index.js", 25 | "./schema/*": "./dist/schema/*.js", 26 | "./transactions/*": "./dist/transactions/*.js", 27 | "./types": "./dist/types.js", 28 | "./utils": "./dist/utils.js", 29 | "./package.json": "./package.json" 30 | }, 31 | "typesVersions": { 32 | "*": { 33 | "*": [ 34 | "./dist/*" 35 | ], 36 | "schema": [ 37 | "./dist/schema/index.d.ts" 38 | ] 39 | } 40 | }, 41 | "files": [ 42 | "/dist" 43 | ], 44 | "scripts": { 45 | "build": "tsc -p tsconfig.build.json && tsc-alias && ts-add-js-extension add --dir dist", 46 | "deploy": "node --loader=./loader.js src/cli/bootstrap.ts", 47 | "deploy-kolour-nft": "node --loader=./loader.js src/cli/bootstrap-kolour-nft.ts", 48 | "meta-protocol:propose": "node --loader=./loader.js src/cli/meta-protocol/propose", 49 | "meta-protocol:apply": "node --loader=./loader.js src/cli/meta-protocol/apply", 50 | "protocol:propose": "node --loader=./loader.js src/cli/protocol/propose", 51 | "protocol:apply": "node --loader=./loader.js src/cli/protocol/apply", 52 | "lint": "eslint src tests", 53 | "lint:fix": "eslint --fix src tests", 54 | "prepare": "npm run build", 55 | "test": "NODE_NO_WARNINGS=1 NODE_OPTIONS=--experimental-vm-modules jest", 56 | "typecheck": "tsc --noEmit --strict", 57 | "typecheck:watch": "tsc --noEmit --strict --watch" 58 | }, 59 | "dependencies": { 60 | "@hyperionbt/helios": "^0.13.10", 61 | "@shinka-network/json-bigint": "^1.0.2", 62 | "@sinclair/typebox": "^0.26.8", 63 | "deep-equal": "^2.2.0", 64 | "lucid-cardano": "^0.9.4" 65 | }, 66 | "devDependencies": { 67 | "@types/deep-equal": "^1.0.1", 68 | "@types/jest": "^29.5.0", 69 | "@types/node": "^18.15.11", 70 | "@typescript-eslint/eslint-plugin": "^5.57.0", 71 | "@typescript-eslint/parser": "^5.57.0", 72 | "eslint": "^8.37.0", 73 | "eslint-config-prettier": "^8.8.0", 74 | "eslint-import-resolver-typescript": "^3.5.4", 75 | "eslint-plugin-import": "^2.27.5", 76 | "eslint-plugin-jest": "^27.2.1", 77 | "eslint-plugin-jest-formatting": "^3.1.0", 78 | "eslint-plugin-prettier": "^4.2.1", 79 | "prettier": "^2.8.7", 80 | "sort-package-json": "^2.4.1", 81 | "ts-add-js-extension": "^1.3.3", 82 | "ts-jest": "^29.1.0", 83 | "ts-node": "^10.9.1", 84 | "tsc-alias": "^1.8.5", 85 | "tsconfig-paths": "^4.2.0", 86 | "typescript": "^5.0.3" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/transactions/treasury/dedicated-treasury/revoke.ts: -------------------------------------------------------------------------------- 1 | import { Lucid, UTxO } from "lucid-cardano"; 2 | 3 | import { addressFromScriptHashes } from "@/helpers/lucid"; 4 | import { constructTxOutputId } from "@/helpers/schema"; 5 | import * as S from "@/schema"; 6 | import { ProtocolParamsDatum } from "@/schema/teiki/protocol"; 7 | import { 8 | DedicatedTreasuryRedeemer, 9 | OpenTreasuryDatum, 10 | } from "@/schema/teiki/treasury"; 11 | import { 12 | MIN_UTXO_LOVELACE, 13 | RATIO_MULTIPLIER, 14 | TREASURY_REVOKE_DISCOUNT_CENTS, 15 | } from "@/transactions/constants"; 16 | import { assert } from "@/utils"; 17 | 18 | export type Params = { 19 | protocolParamsUtxo: UTxO; 20 | // Not need to reference project UTxO in case of initiate or cancel delist 21 | projectUtxo?: UTxO; 22 | dedicatedTreasuryUtxos: UTxO[]; 23 | dedicatedTreasuryVRefScriptUtxo: UTxO; 24 | }; 25 | 26 | export function revokeTx( 27 | lucid: Lucid, 28 | { 29 | protocolParamsUtxo, 30 | projectUtxo, 31 | dedicatedTreasuryUtxos, 32 | dedicatedTreasuryVRefScriptUtxo, 33 | }: Params 34 | ) { 35 | assert( 36 | protocolParamsUtxo.datum != null, 37 | "Invalid protocol params UTxO: Missing inline datum" 38 | ); 39 | 40 | const protocolParams = S.fromData( 41 | S.fromCbor(protocolParamsUtxo.datum), 42 | ProtocolParamsDatum 43 | ); 44 | 45 | let tx = lucid.newTx(); 46 | tx = projectUtxo 47 | ? tx.readFrom([ 48 | protocolParamsUtxo, 49 | dedicatedTreasuryVRefScriptUtxo, 50 | projectUtxo, 51 | ]) 52 | : tx.readFrom([protocolParamsUtxo, dedicatedTreasuryVRefScriptUtxo]); 53 | 54 | const dedicatedTreasuryRedeemer = S.toCbor( 55 | S.toData({ case: "Revoke" }, DedicatedTreasuryRedeemer) 56 | ); 57 | 58 | const openTreasuryVScriptAddress = addressFromScriptHashes( 59 | lucid, 60 | protocolParams.registry.openTreasuryValidator.latest.script.hash, 61 | protocolParams.registry.protocolStakingValidator.script.hash 62 | ); 63 | 64 | for (const dedicatedTreasuryUTxO of dedicatedTreasuryUtxos) { 65 | assert( 66 | dedicatedTreasuryUTxO.datum != null, 67 | "Invalid open treasury UTxO: Missing inline datum" 68 | ); 69 | 70 | tx = tx.collectFrom([dedicatedTreasuryUTxO], dedicatedTreasuryRedeemer); 71 | 72 | const spendingAda = BigInt(dedicatedTreasuryUTxO.assets.lovelace); 73 | const adaToTreasury = 74 | spendingAda - 75 | protocolParams.discountCentPrice * TREASURY_REVOKE_DISCOUNT_CENTS; 76 | 77 | if (adaToTreasury > 0n) { 78 | const treasuryAda = 79 | adaToTreasury > MIN_UTXO_LOVELACE ? adaToTreasury : MIN_UTXO_LOVELACE; 80 | const openTreasuryDatum: OpenTreasuryDatum = { 81 | governorAda: 82 | (treasuryAda * protocolParams.governorShareRatio) / RATIO_MULTIPLIER, 83 | tag: { 84 | kind: "TagContinuation", 85 | former: constructTxOutputId(dedicatedTreasuryUTxO), 86 | }, 87 | }; 88 | tx = tx.payToContract( 89 | openTreasuryVScriptAddress, 90 | { 91 | inline: S.toCbor(S.toData(openTreasuryDatum, OpenTreasuryDatum)), 92 | }, 93 | { lovelace: treasuryAda } 94 | ); 95 | } 96 | } 97 | 98 | return tx; 99 | } 100 | -------------------------------------------------------------------------------- /src/contracts/compile.ts: -------------------------------------------------------------------------------- 1 | import { UplcProgram, bytesToHex } from "@hyperionbt/helios"; 2 | import { Data, Script } from "lucid-cardano"; 3 | 4 | import { toDataJson } from "@/schema"; 5 | import { Hex } from "@/types"; 6 | 7 | import modBackingVTypes from "./backing/backing.v/types"; 8 | import modProofOfBackingMpTypes from "./backing/proof-of-backing.mp/types"; 9 | import modConstants from "./common/constants"; 10 | import modFraction from "./common/fraction"; 11 | import modHelpers from "./common/helpers"; 12 | import modCommonTypes from "./common/types"; 13 | import modTeikiPlantNftTypes from "./meta-protocol/teiki-plant.nft/types"; 14 | import modTeikiPlantVTypes from "./meta-protocol/teiki-plant.v/types"; 15 | import modTeikiMpTypes from "./meta-protocol/teiki.mp/types"; 16 | import { loadModules, HeliosScript, newProgram } from "./program"; 17 | import modProjectDetailVTypes from "./project/project-detail.v/types"; 18 | import modProjectScriptVTypes from "./project/project-script.v/types"; 19 | import modProjectAtTypes from "./project/project.at/types"; 20 | import modProjectVTypes from "./project/project.v/types"; 21 | import modProtocolParamsVTypes from "./protocol/protocol-params.v/types"; 22 | import modProtocolProposalVTypes from "./protocol/protocol-proposal.v/types"; 23 | import modProtocolNftTypes from "./protocol/protocol.nft/types"; 24 | import modDedicatedTreasuryVTypes from "./treasury/dedicated-treasury.v/types"; 25 | import modOpenTreasuryVTypes from "./treasury/open-treasury.v/types"; 26 | import modSharedTreasuryVTypes from "./treasury/shared-treasury.v/types"; 27 | 28 | type CompileOptions = { 29 | simplify?: boolean; 30 | parameters?: Record; 31 | }; 32 | 33 | let defaultOptions: Omit = {}; 34 | 35 | export function setDefaultOptions( 36 | options: Omit, 37 | replace = false 38 | ) { 39 | if (replace) defaultOptions = options; 40 | else defaultOptions = { ...defaultOptions, ...options }; 41 | } 42 | 43 | setDefaultOptions({ simplify: true }); 44 | 45 | const HELIOS_MODULES = loadModules([ 46 | modConstants, 47 | modCommonTypes, 48 | modFraction, 49 | modHelpers, 50 | modTeikiMpTypes, 51 | modTeikiPlantNftTypes, 52 | modTeikiPlantVTypes, 53 | modProtocolParamsVTypes, 54 | modProtocolProposalVTypes, 55 | modProtocolNftTypes, 56 | modProjectAtTypes, 57 | modProjectVTypes, 58 | modProjectDetailVTypes, 59 | modProjectScriptVTypes, 60 | modBackingVTypes, 61 | modProofOfBackingMpTypes, 62 | modDedicatedTreasuryVTypes, 63 | modSharedTreasuryVTypes, 64 | modOpenTreasuryVTypes, 65 | ]); 66 | 67 | export function compile( 68 | main: HeliosScript, 69 | options?: CompileOptions 70 | ): UplcProgram { 71 | const opts = { ...defaultOptions, ...options }; 72 | const program = newProgram(main, HELIOS_MODULES); 73 | if (opts.parameters) 74 | Object.entries(opts.parameters).forEach(([name, value]) => 75 | program.changeParam(name, toDataJson(value)) 76 | ); 77 | return program.compile(main.simplify ?? opts.simplify); 78 | } 79 | 80 | export function exportScript(uplcProgram: UplcProgram): Script { 81 | return { 82 | type: "PlutusV2" as const, 83 | script: serialize(uplcProgram), 84 | }; 85 | } 86 | 87 | export function serialize(uplcProgram: UplcProgram): Hex { 88 | return bytesToHex(uplcProgram.toCbor()); 89 | } 90 | -------------------------------------------------------------------------------- /src/contracts/protocol/protocol.sv/main.ts: -------------------------------------------------------------------------------- 1 | import { Hex } from "@/types"; 2 | 3 | import { header, helios, HeliosScript, module } from "../../program"; 4 | 5 | export type Params = { 6 | protocolNftMph: Hex; 7 | }; 8 | 9 | export default function main({ protocolNftMph }: Params): HeliosScript { 10 | return helios` 11 | ${header("staking", "sv__protocol")} 12 | 13 | import { RATIO_MULTIPLIER } 14 | from ${module("constants")} 15 | 16 | import { 17 | is_tx_authorized_by, 18 | find_pparams_datum_from_inputs, 19 | find_pparams_datum_from_outputs, 20 | staking_credential_to_validator_hash, 21 | script_hash_to_staking_credential 22 | } from ${module("helpers")} 23 | 24 | import { Datum as OpenTreasuryDatum } 25 | from ${module("v__open_treasury__types")} 26 | 27 | import { Datum as PParamsDatum } 28 | from ${module("v__protocol_params__types")} 29 | 30 | const PROTOCOL_NFT_MPH: MintingPolicyHash = 31 | MintingPolicyHash::new(#${protocolNftMph}) 32 | 33 | func main(_, ctx: ScriptContext) -> Bool { 34 | tx: Tx = ctx.tx; 35 | 36 | ctx.get_script_purpose().switch { 37 | 38 | rewarding: Rewarding => { 39 | pparams_datum: PParamsDatum = 40 | find_pparams_datum_from_inputs(tx.ref_inputs, PROTOCOL_NFT_MPH); 41 | 42 | own_staking_credential: StakingCredential = rewarding.credential; 43 | 44 | withdrawn_amount: Int = tx.withdrawals.get(own_staking_credential); 45 | 46 | governor_share_amount: Int = 47 | withdrawn_amount * pparams_datum.governor_share_ratio / RATIO_MULTIPLIER; 48 | 49 | open_treasury_address: Address = Address::new( 50 | Credential::new_validator( 51 | pparams_datum.registry.open_treasury_validator.latest 52 | ), 53 | Option[StakingCredential]::Some { 54 | script_hash_to_staking_credential( 55 | pparams_datum.registry.protocol_staking_validator 56 | ) 57 | } 58 | ); 59 | 60 | open_treasury_output: TxOutput = 61 | tx.outputs.find( 62 | (output: TxOutput) -> { 63 | output.address == open_treasury_address 64 | && output.value.to_map().length == 1 65 | && output.value.get(AssetClass::ADA) >= withdrawn_amount 66 | } 67 | ); 68 | 69 | open_treasury_datum: OpenTreasuryDatum = 70 | open_treasury_output.datum.switch { 71 | i: Inline => OpenTreasuryDatum::from_data(i.data), 72 | else => error("Invalid open treasury utxo: must inline datum") 73 | }; 74 | 75 | open_treasury_datum.governor_ada == governor_share_amount 76 | && open_treasury_datum.tag.switch { 77 | tag: TagProtocolStakingRewards => { 78 | tag.staking_validator == staking_credential_to_validator_hash(own_staking_credential) 79 | }, 80 | else => false 81 | } 82 | }, 83 | 84 | certifying: Certifying => { 85 | pparams_datum: PParamsDatum = 86 | find_pparams_datum_from_outputs( 87 | tx.ref_inputs.map((input: TxInput) -> { input.output }) 88 | + tx.outputs, 89 | PROTOCOL_NFT_MPH 90 | ); 91 | 92 | is_authorized: Bool = 93 | is_tx_authorized_by(tx, pparams_datum.staking_manager) 94 | || is_tx_authorized_by(tx, pparams_datum.governor_address.credential); 95 | 96 | certifying.dcert.switch { 97 | Register => error("unreachable"), 98 | Deregister => is_authorized, 99 | Delegate => is_authorized, 100 | else => false 101 | } 102 | }, 103 | 104 | else => false 105 | } 106 | } 107 | `; 108 | } 109 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { TxComplete } from "lucid-cardano"; 2 | 3 | import { getMigratableScript } from "@/commands/generate-protocol-params"; 4 | import { Registry } from "@/schema/teiki/protocol"; 5 | import { MIN_UTXO_LOVELACE } from "@/transactions/constants"; 6 | import { Hex } from "@/types"; 7 | 8 | import { generateBlake2b224Hash } from "./emulator"; 9 | 10 | export type ValidatorScriptHashRegistry = { 11 | project?: Hex; 12 | projectDetail?: Hex; 13 | projectScript?: Hex; 14 | backing?: Hex; 15 | dedicatedTreasury?: Hex; 16 | openTreasury?: Hex; 17 | sharedTreasury?: Hex; 18 | }; 19 | 20 | export function generateProtocolRegistry( 21 | protocolSvHash: Hex, 22 | validatorScriptHashRegistry?: ValidatorScriptHashRegistry 23 | ): Registry { 24 | return { 25 | protocolStakingValidator: { script: { hash: protocolSvHash } }, 26 | projectValidator: getMigratableScript( 27 | validatorScriptHashRegistry?.project 28 | ? validatorScriptHashRegistry?.project 29 | : generateBlake2b224Hash() 30 | ), 31 | projectDetailValidator: getMigratableScript( 32 | validatorScriptHashRegistry?.projectDetail 33 | ? validatorScriptHashRegistry?.projectDetail 34 | : generateBlake2b224Hash() 35 | ), 36 | projectScriptValidator: getMigratableScript( 37 | validatorScriptHashRegistry?.projectScript 38 | ? validatorScriptHashRegistry?.projectScript 39 | : generateBlake2b224Hash() 40 | ), 41 | backingValidator: getMigratableScript( 42 | validatorScriptHashRegistry?.backing 43 | ? validatorScriptHashRegistry?.backing 44 | : generateBlake2b224Hash() 45 | ), 46 | dedicatedTreasuryValidator: getMigratableScript( 47 | validatorScriptHashRegistry?.dedicatedTreasury 48 | ? validatorScriptHashRegistry?.dedicatedTreasury 49 | : generateBlake2b224Hash() 50 | ), 51 | sharedTreasuryValidator: getMigratableScript( 52 | validatorScriptHashRegistry?.sharedTreasury 53 | ? validatorScriptHashRegistry?.sharedTreasury 54 | : generateBlake2b224Hash() 55 | ), 56 | openTreasuryValidator: getMigratableScript( 57 | validatorScriptHashRegistry?.openTreasury 58 | ? validatorScriptHashRegistry?.openTreasury 59 | : generateBlake2b224Hash() 60 | ), 61 | }; 62 | } 63 | 64 | export function getRandomLovelaceAmount(max?: number) { 65 | const random = BigInt(Math.floor(Math.random() * (max ?? 1_000_000_000))); 66 | return random > MIN_UTXO_LOVELACE ? random : MIN_UTXO_LOVELACE; 67 | } 68 | 69 | export function printExUnits(tx: TxComplete) { 70 | const lines: string[] = []; 71 | const txCmp = tx.txComplete; 72 | const redeemers = txCmp.witness_set().redeemers(); 73 | const fmt = Intl.NumberFormat("en-US").format; 74 | lines.push(`Tx Id :: ${tx.toHash()}`); 75 | lines.push(`Tx Size :: ${fmt(txCmp.to_bytes().length)}`); 76 | lines.push(`Tx Fee :: ${fmt(BigInt(txCmp.body().fee().to_str()))}`); 77 | if (redeemers == null) { 78 | lines.push(`Ex Units :: ∅`); 79 | } else { 80 | const fmtExUnits = ({ mem, cpu }: { mem: bigint; cpu: bigint }) => 81 | `{ Mem = ${fmt(mem)} | Cpu = ${fmt(cpu)} }`; 82 | 83 | const summary: { tag: string; index: bigint; mem: bigint; cpu: bigint }[] = 84 | []; 85 | for (let i = 0, n = redeemers.len(); i < n; i++) { 86 | const rdm = redeemers.get(i); 87 | const tag = REDEEMER_TAG_KIND[rdm.tag().kind()]; 88 | const index = BigInt(rdm.index().to_str()); 89 | const ex = rdm.ex_units(); 90 | const mem = BigInt(ex.mem().to_str()); 91 | const cpu = BigInt(ex.steps().to_str()); 92 | summary.push({ tag, index, mem, cpu }); 93 | } 94 | const total = { mem: 0n, cpu: 0n }; 95 | for (const e of summary) { 96 | total.mem += e.mem; 97 | total.cpu += e.cpu; 98 | } 99 | lines.push(`Ex Units :: ${fmtExUnits(total)}`); 100 | for (const e of summary) 101 | lines.push(` ${e.tag} ! ${e.index} ~ ${fmtExUnits(e)}`); 102 | } 103 | console.info(lines.join("\n")); 104 | } 105 | 106 | const REDEEMER_TAG_KIND = ["spend", "mint", "cert", "reward"]; 107 | -------------------------------------------------------------------------------- /src/helpers/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | C, 4 | Credential, 5 | fromHex, 6 | getAddressDetails, 7 | Lucid, 8 | OutRef, 9 | toHex, 10 | } from "lucid-cardano"; 11 | 12 | import * as S from "@/schema"; 13 | import { Plant } from "@/schema/teiki/backing"; 14 | import { MigratableScript } from "@/schema/teiki/protocol"; 15 | import { Hex } from "@/types"; 16 | import { assert } from "@/utils"; 17 | 18 | export function constructTxOutputId({ 19 | txHash, 20 | outputIndex, 21 | }: OutRef): S.TxOutputId { 22 | return { 23 | txId: txHash, 24 | index: BigInt(outputIndex), 25 | }; 26 | } 27 | 28 | export function constructPlantHashUsingBlake2b(plant: Plant) { 29 | const cbor = S.toCbor(S.toData(plant, Plant)); 30 | return hashBlake2b256(cbor); 31 | } 32 | 33 | export function constructProjectIdUsingBlake2b(ref: OutRef): Hex { 34 | const cbor = S.toCbor(S.toData(constructTxOutputId(ref), S.TxOutputId)); 35 | return hashBlake2b256(cbor); 36 | } 37 | 38 | export function constructAssetClass( 39 | mintingPolicyHash: Hex, 40 | tokenName: Hex 41 | ): S.AssetClass { 42 | return { 43 | mintingPolicyHash: { script: { hash: mintingPolicyHash } }, 44 | tokenName: tokenName, 45 | }; 46 | } 47 | 48 | export function constructMigratableScript( 49 | latestScriptHash: Hex, 50 | migrations: Record 51 | ): MigratableScript { 52 | return { 53 | latest: { script: { hash: latestScriptHash } }, 54 | migrations: Object.entries(migrations).map( 55 | ([migratingScriptHash, { mintingPolicyHash, tokenName }]) => [ 56 | { script: { hash: migratingScriptHash } }, 57 | constructAssetClass(mintingPolicyHash, tokenName), 58 | ] 59 | ), 60 | }; 61 | } 62 | 63 | export function constructAddress(address: Address): S.Address { 64 | const { paymentCredential, stakeCredential } = getAddressDetails(address); 65 | assert(paymentCredential, "Cannot extract payment credential from address"); 66 | const scPaymentCredential: S.PaymentCredential = 67 | paymentCredential.type === "Key" 68 | ? { type: "PubKey", key: { hash: paymentCredential.hash } } 69 | : { type: "Validator", script: { hash: paymentCredential.hash } }; 70 | const scStakingCredential: S.StakingCredential | null = stakeCredential 71 | ? stakeCredential.type === "Key" 72 | ? { 73 | kind: "Hash", 74 | type: "StakeKey", 75 | key: { hash: stakeCredential.hash }, 76 | } 77 | : { 78 | kind: "Hash", 79 | type: "Validator", 80 | script: { hash: stakeCredential.hash }, 81 | } 82 | : null; 83 | return { 84 | paymentCredential: scPaymentCredential, 85 | stakingCredential: scStakingCredential, 86 | }; 87 | } 88 | 89 | // TODO: We shouldn't rely on this function for transaction building 90 | // We should support other kinds of credential in the future. 91 | export function extractPaymentPubKeyHash(scAddress: S.Address): Hex { 92 | const { paymentCredential } = scAddress; 93 | assert( 94 | paymentCredential.type === "PubKey", 95 | "Address must have a public-key hash payment credential" 96 | ); 97 | return paymentCredential.key.hash; 98 | } 99 | 100 | export function deconstructAddress( 101 | lucid: Lucid, 102 | scAddress: S.Address 103 | ): Address { 104 | const { paymentCredential, stakingCredential } = scAddress; 105 | const lcPaymentCredential: Credential = 106 | paymentCredential.type === "PubKey" 107 | ? { type: "Key", hash: paymentCredential.key.hash } 108 | : { type: "Script", hash: paymentCredential.script.hash }; 109 | const lcStakingCredential: Credential | undefined = 110 | stakingCredential && stakingCredential.kind === "Hash" 111 | ? stakingCredential.type === "StakeKey" 112 | ? { type: "Key", hash: stakingCredential.key.hash } 113 | : { type: "Script", hash: stakingCredential.script.hash } 114 | : undefined; 115 | return lucid.utils.credentialToAddress( 116 | lcPaymentCredential, 117 | lcStakingCredential 118 | ); 119 | } 120 | 121 | export function hashBlake2b256(cbor: Hex): Hex { 122 | return toHex(C.hash_blake2b256(fromHex(cbor))); 123 | } 124 | -------------------------------------------------------------------------------- /src/transactions/treasury/open-treasury/withdraw-ada.ts: -------------------------------------------------------------------------------- 1 | import { Lucid, UTxO } from "lucid-cardano"; 2 | 3 | import { 4 | constructTxOutputId, 5 | deconstructAddress, 6 | extractPaymentPubKeyHash, 7 | } from "@/helpers/schema"; 8 | import * as S from "@/schema"; 9 | import { ProtocolParamsDatum } from "@/schema/teiki/protocol"; 10 | import { UserTag } from "@/schema/teiki/tags"; 11 | import { 12 | OpenTreasuryDatum, 13 | OpenTreasuryRedeemer, 14 | } from "@/schema/teiki/treasury"; 15 | import { 16 | RATIO_MULTIPLIER, 17 | TREASURY_UTXO_MIN_ADA, 18 | TREASURY_WITHDRAWAL_DISCOUNT_RATIO, 19 | } from "@/transactions/constants"; 20 | import { Actor } from "@/types"; 21 | import { assert } from "@/utils"; 22 | 23 | export type Params = { 24 | protocolParamsUtxo: UTxO; 25 | openTreasuryUtxos: UTxO[]; 26 | openTreasuryVRefScriptUtxo: UTxO; 27 | actor: Actor; 28 | }; 29 | 30 | export function withdrawAdaTx( 31 | lucid: Lucid, 32 | { 33 | protocolParamsUtxo, 34 | openTreasuryUtxos, 35 | openTreasuryVRefScriptUtxo, 36 | actor, 37 | }: Params 38 | ) { 39 | assert( 40 | protocolParamsUtxo.datum != null, 41 | "Invalid protocol params UTxO: Missing inline datum" 42 | ); 43 | 44 | const protocolParams = S.fromData( 45 | S.fromCbor(protocolParamsUtxo.datum), 46 | ProtocolParamsDatum 47 | ); 48 | 49 | const protocolGovernorPkh = extractPaymentPubKeyHash( 50 | protocolParams.governorAddress 51 | ); 52 | 53 | const openTreasuryRedeemer = S.toCbor( 54 | S.toData({ case: "WithdrawAda" }, OpenTreasuryRedeemer) 55 | ); 56 | 57 | let tx = lucid 58 | .newTx() 59 | .readFrom([protocolParamsUtxo, openTreasuryVRefScriptUtxo]); 60 | 61 | let totalInG = 0n; 62 | let totalInW = 0n; 63 | for (const openTreasuryUtxo of openTreasuryUtxos) { 64 | assert( 65 | openTreasuryUtxo.datum != null, 66 | "Invalid open treasury UTxO: Missing inline datum" 67 | ); 68 | 69 | tx = tx.collectFrom([openTreasuryUtxo], openTreasuryRedeemer); 70 | const openTreasuryDatum = S.fromData( 71 | S.fromCbor(openTreasuryUtxo.datum), 72 | OpenTreasuryDatum 73 | ); 74 | 75 | const inW = BigInt(openTreasuryUtxo.assets.lovelace); 76 | totalInW += inW; 77 | 78 | const maxGovernorAdaWithZero = 79 | openTreasuryDatum.governorAda > 0n ? openTreasuryDatum.governorAda : 0n; 80 | const inG = maxGovernorAdaWithZero < inW ? maxGovernorAdaWithZero : inW; 81 | totalInG += inG; 82 | } 83 | 84 | const outW = totalInW - totalInG; 85 | 86 | const spendingTxOutputId = constructTxOutputId( 87 | ascSortUTxOByOutputId(openTreasuryUtxos)[0] 88 | ); 89 | 90 | const outputOpenTreasuryDatum: OpenTreasuryDatum = { 91 | governorAda: 0n, 92 | tag: { 93 | kind: "TagContinuation", 94 | former: spendingTxOutputId, 95 | }, 96 | }; 97 | 98 | tx = tx.payToContract( 99 | openTreasuryUtxos[0].address, 100 | { 101 | inline: S.toCbor(S.toData(outputOpenTreasuryDatum, OpenTreasuryDatum)), 102 | }, 103 | { lovelace: outW } 104 | ); 105 | 106 | if (actor === "protocol-governor") { 107 | tx = tx.addSignerKey(protocolGovernorPkh); 108 | return tx; 109 | } 110 | 111 | const delta = totalInG; 112 | assert( 113 | delta >= TREASURY_UTXO_MIN_ADA, 114 | "Invalid output open treasury UTxO: require covering min ADA" 115 | ); 116 | 117 | const outputGovernorDatum: UserTag = { 118 | kind: "TagTreasuryWithdrawal", 119 | treasuryOutputId: spendingTxOutputId, 120 | }; 121 | 122 | return tx.payToAddressWithData( 123 | deconstructAddress(lucid, protocolParams.governorAddress), 124 | { inline: S.toCbor(S.toData(outputGovernorDatum, UserTag)) }, 125 | { 126 | lovelace: 127 | (delta * (RATIO_MULTIPLIER - TREASURY_WITHDRAWAL_DISCOUNT_RATIO)) / 128 | RATIO_MULTIPLIER, 129 | } 130 | ); 131 | } 132 | 133 | //https://www.hyperion-bt.org/helios-book/lang/builtins/txoutputid.html 134 | export function ascSortUTxOByOutputId(utxos: UTxO[]) { 135 | return utxos.sort((o1, o2) => { 136 | if (o1.txHash == o2.txHash) return o1.outputIndex > o2.outputIndex ? 1 : -1; 137 | 138 | return o1.txHash > o2.txHash ? 1 : -1; 139 | }); 140 | } 141 | -------------------------------------------------------------------------------- /src/transactions/treasury/dedicated-treasury/withdraw-ada.ts: -------------------------------------------------------------------------------- 1 | import { Lucid, UTxO } from "lucid-cardano"; 2 | 3 | import { 4 | constructTxOutputId, 5 | deconstructAddress, 6 | extractPaymentPubKeyHash, 7 | } from "@/helpers/schema"; 8 | import * as S from "@/schema"; 9 | import { ProtocolParamsDatum } from "@/schema/teiki/protocol"; 10 | import { UserTag } from "@/schema/teiki/tags"; 11 | import { 12 | DedicatedTreasuryDatum, 13 | DedicatedTreasuryRedeemer, 14 | } from "@/schema/teiki/treasury"; 15 | import { 16 | RATIO_MULTIPLIER, 17 | TREASURY_UTXO_MIN_ADA, 18 | TREASURY_WITHDRAWAL_DISCOUNT_RATIO, 19 | } from "@/transactions/constants"; 20 | import { Actor } from "@/types"; 21 | import { assert } from "@/utils"; 22 | 23 | export type Params = { 24 | protocolParamsUtxo: UTxO; 25 | // Not need to reference project UTxO in case of initiate or cancel delist 26 | projectUtxo?: UTxO; 27 | dedicatedTreasuryUtxos: UTxO[]; 28 | dedicatedTreasuryVRefScriptUtxo: UTxO; 29 | actor: Actor; 30 | }; 31 | 32 | export function withdrawAdaTx( 33 | lucid: Lucid, 34 | { 35 | protocolParamsUtxo, 36 | projectUtxo, 37 | dedicatedTreasuryUtxos, 38 | dedicatedTreasuryVRefScriptUtxo, 39 | actor, 40 | }: Params 41 | ) { 42 | assert( 43 | protocolParamsUtxo.datum != null, 44 | "Invalid protocol params UTxO: Missing inline datum" 45 | ); 46 | 47 | const protocolParams = S.fromData( 48 | S.fromCbor(protocolParamsUtxo.datum), 49 | ProtocolParamsDatum 50 | ); 51 | 52 | const protocolGovernorPkh = extractPaymentPubKeyHash( 53 | protocolParams.governorAddress 54 | ); 55 | 56 | let tx = lucid.newTx(); 57 | tx = projectUtxo 58 | ? tx.readFrom([ 59 | protocolParamsUtxo, 60 | dedicatedTreasuryVRefScriptUtxo, 61 | projectUtxo, 62 | ]) 63 | : tx.readFrom([protocolParamsUtxo, dedicatedTreasuryVRefScriptUtxo]); 64 | 65 | const minRemainingAda = 66 | (1n + protocolParams.minTreasuryPerMilestoneEvent) * TREASURY_UTXO_MIN_ADA; 67 | 68 | const dedicatedTreasuryRedeemer = S.toCbor( 69 | S.toData({ case: "WithdrawAda" }, DedicatedTreasuryRedeemer) 70 | ); 71 | 72 | for (const dedicatedTreasuryUTxO of dedicatedTreasuryUtxos) { 73 | assert( 74 | dedicatedTreasuryUTxO.datum != null, 75 | "Invalid open treasury UTxO: Missing inline datum" 76 | ); 77 | const dedicatedTreasuryDatum = S.fromData( 78 | S.fromCbor(dedicatedTreasuryUTxO.datum), 79 | DedicatedTreasuryDatum 80 | ); 81 | 82 | const spendingAda = BigInt(dedicatedTreasuryUTxO.assets.lovelace); 83 | const governorAda = dedicatedTreasuryDatum.governorAda; 84 | const withdrawAda = 85 | governorAda < spendingAda - minRemainingAda 86 | ? governorAda 87 | : spendingAda - minRemainingAda; 88 | 89 | const outputDatum: DedicatedTreasuryDatum = { 90 | projectId: dedicatedTreasuryDatum.projectId, 91 | governorAda: governorAda - withdrawAda, 92 | tag: { 93 | kind: "TagContinuation", 94 | former: constructTxOutputId(dedicatedTreasuryUTxO), 95 | }, 96 | }; 97 | 98 | tx = tx 99 | .collectFrom([dedicatedTreasuryUTxO], dedicatedTreasuryRedeemer) 100 | .payToContract( 101 | dedicatedTreasuryUTxO.address, 102 | { 103 | inline: S.toCbor(S.toData(outputDatum, DedicatedTreasuryDatum)), 104 | }, 105 | { lovelace: spendingAda - withdrawAda } 106 | ); 107 | if (actor === "protocol-governor") { 108 | assert(withdrawAda > 0n, "Withdraw ADA amount must be larger than zero"); 109 | tx = tx.addSignerKey(protocolGovernorPkh); 110 | } else { 111 | assert( 112 | withdrawAda > TREASURY_UTXO_MIN_ADA, 113 | "Withdraw ADA amount must be large than min UTxO ADA" 114 | ); 115 | 116 | const outputGovernorDatum: UserTag = { 117 | kind: "TagTreasuryWithdrawal", 118 | treasuryOutputId: constructTxOutputId(dedicatedTreasuryUTxO), 119 | }; 120 | 121 | tx = tx.payToAddressWithData( 122 | deconstructAddress(lucid, protocolParams.governorAddress), 123 | { inline: S.toCbor(S.toData(outputGovernorDatum, UserTag)) }, 124 | { 125 | lovelace: 126 | (withdrawAda * 127 | (RATIO_MULTIPLIER - TREASURY_WITHDRAWAL_DISCOUNT_RATIO)) / 128 | RATIO_MULTIPLIER, 129 | } 130 | ); 131 | } 132 | } 133 | 134 | return tx; 135 | } 136 | -------------------------------------------------------------------------------- /src/contracts/common/constants.ts: -------------------------------------------------------------------------------- 1 | import { fromText } from "lucid-cardano"; 2 | 3 | import { 4 | INACTIVE_PROJECT_UTXO_ADA, 5 | PROJECT_CLOSE_DISCOUNT_CENTS, 6 | PROJECT_DELIST_DISCOUNT_CENTS, 7 | PROJECT_DETAIL_UTXO_ADA, 8 | PROJECT_FUNDS_WITHDRAWAL_DISCOUNT_RATIO, 9 | PROJECT_NEW_MILESTONE_DISCOUNT_CENTS, 10 | PROJECT_SCRIPT_CLOSE_DISCOUNT_CENTS, 11 | PROJECT_SCRIPT_DELIST_DISCOUNT_CENTS, 12 | PROJECT_SCRIPT_UTXO_ADA, 13 | RATIO_MULTIPLIER, 14 | TREASURY_REVOKE_DISCOUNT_CENTS, 15 | TREASURY_UTXO_MIN_ADA, 16 | TREASURY_WITHDRAWAL_DISCOUNT_RATIO, 17 | TREASURY_MIN_WITHDRAWAL_ADA, 18 | PROJECT_IMMEDIATE_CLOSURE_TX_TIME_SLIPPAGE, 19 | PROOF_OF_BACKING_PLANT_TX_TIME_SLIPPAGE, 20 | PROJECT_SPONSORSHIP_RESOLUTION, 21 | } from "@/transactions/constants"; 22 | import { Hex } from "@/types"; 23 | 24 | import { header, helios } from "../program"; 25 | 26 | // FIXME: Redo this module... 27 | 28 | export const PROTOCOL_NFT_TOKEN_NAMES = { 29 | PARAMS: fromText("params"), 30 | PROPOSAL: fromText("proposal"), 31 | }; 32 | 33 | export const PROJECT_AT_TOKEN_NAMES = { 34 | PROJECT: fromText("project"), 35 | PROJECT_DETAIL: fromText("project-detail"), 36 | PROJECT_SCRIPT: fromText("project-script"), 37 | }; 38 | 39 | export const PROOF_OF_BACKING_TOKEN_NAMES = { 40 | SEED: fromText("seed"), 41 | WILTED_FLOWER: fromText("wilted-flower"), 42 | }; 43 | 44 | export const TEIKI_TOKEN_NAME: Hex = fromText("teiki"); 45 | 46 | export const TEIKI_PLANT_NFT_TOKEN_NAME: Hex = fromText("teiki-plant"); 47 | 48 | export const MIGRATE_TOKEN_NAME: Hex = fromText("migration"); 49 | 50 | export const INACTIVE_BACKING_CLEANUP_DISCOUNT_CENTS = 20n; 51 | 52 | export default helios` 53 | ${header("module", "constants")} 54 | 55 | const ADA_MINTING_POLICY_HASH: MintingPolicyHash = MintingPolicyHash::new(#) 56 | 57 | const ADA_TOKEN_NAME: ByteArray = # 58 | 59 | const PROTOCOL_PARAMS_NFT_TOKEN_NAME: ByteArray = 60 | #${PROTOCOL_NFT_TOKEN_NAMES.PARAMS} 61 | 62 | const PROTOCOL_PROPOSAL_NFT_TOKEN_NAME: ByteArray = 63 | #${PROTOCOL_NFT_TOKEN_NAMES.PROPOSAL} 64 | 65 | const PROJECT_AT_TOKEN_NAME: ByteArray = 66 | #${PROJECT_AT_TOKEN_NAMES.PROJECT} 67 | 68 | const PROJECT_DETAIL_AT_TOKEN_NAME: ByteArray = 69 | #${PROJECT_AT_TOKEN_NAMES.PROJECT_DETAIL} 70 | 71 | const PROJECT_SCRIPT_AT_TOKEN_NAME: ByteArray = 72 | #${PROJECT_AT_TOKEN_NAMES.PROJECT_SCRIPT} 73 | 74 | const TEIKI_TOKEN_NAME: ByteArray = #${TEIKI_TOKEN_NAME} 75 | 76 | const TEIKI_PLANT_NFT_TOKEN_NAME: ByteArray = #${TEIKI_PLANT_NFT_TOKEN_NAME} 77 | 78 | const RATIO_MULTIPLIER: Int = ${RATIO_MULTIPLIER} 79 | 80 | // Project constants 81 | const INACTIVE_PROJECT_UTXO_ADA: Int = ${INACTIVE_PROJECT_UTXO_ADA} 82 | 83 | const PROJECT_CLOSE_DISCOUNT_CENTS: Int = ${PROJECT_CLOSE_DISCOUNT_CENTS} 84 | 85 | const PROJECT_DELIST_DISCOUNT_CENTS: Int = ${PROJECT_DELIST_DISCOUNT_CENTS} 86 | 87 | const PROJECT_DETAIL_UTXO_ADA: Int = ${PROJECT_DETAIL_UTXO_ADA} 88 | 89 | const PROJECT_FUNDS_WITHDRAWAL_DISCOUNT_RATIO: Int = ${PROJECT_FUNDS_WITHDRAWAL_DISCOUNT_RATIO} 90 | 91 | const PROJECT_IMMEDIATE_CLOSURE_TX_TIME_SLIPPAGE: Duration 92 | = Duration::new(${PROJECT_IMMEDIATE_CLOSURE_TX_TIME_SLIPPAGE}) 93 | 94 | const PROJECT_NEW_MILESTONE_DISCOUNT_CENTS: Int = ${PROJECT_NEW_MILESTONE_DISCOUNT_CENTS} 95 | 96 | const PROJECT_SCRIPT_CLOSE_DISCOUNT_CENTS: Int = ${PROJECT_SCRIPT_CLOSE_DISCOUNT_CENTS} 97 | 98 | const PROJECT_SCRIPT_DELIST_DISCOUNT_CENTS: Int = ${PROJECT_SCRIPT_DELIST_DISCOUNT_CENTS} 99 | 100 | const PROJECT_SCRIPT_UTXO_ADA: Int = ${PROJECT_SCRIPT_UTXO_ADA} 101 | 102 | const PROJECT_SPONSORSHIP_RESOLUTION: Duration = Duration::new(${PROJECT_SPONSORSHIP_RESOLUTION}) 103 | 104 | const PROJECT_AT_MIGRATE_IN: Option[MintingPolicyHash] = Option[MintingPolicyHash]::None 105 | 106 | // Backing 107 | const INACTIVE_BACKING_CLEANUP_DISCOUNT_CENTS: Int = ${INACTIVE_BACKING_CLEANUP_DISCOUNT_CENTS} 108 | 109 | const PROOF_OF_BACKING_PLANT_TX_TIME_SLIPPAGE: Duration 110 | = Duration::new(${PROOF_OF_BACKING_PLANT_TX_TIME_SLIPPAGE}) 111 | 112 | const PROOF_OF_BACKING_MIGRATE_IN: Option[MintingPolicyHash] = Option[MintingPolicyHash]::None 113 | 114 | // Treasury 115 | const TREASURY_MIN_WITHDRAWAL_ADA: Int = ${TREASURY_MIN_WITHDRAWAL_ADA} 116 | 117 | const TREASURY_WITHDRAWAL_DISCOUNT_RATIO: Int = ${TREASURY_WITHDRAWAL_DISCOUNT_RATIO} 118 | 119 | const TREASURY_REVOKE_DISCOUNT_CENTS: Int = ${TREASURY_REVOKE_DISCOUNT_CENTS} 120 | 121 | const TREASURY_UTXO_MIN_ADA: Int = ${TREASURY_UTXO_MIN_ADA} 122 | `; 123 | -------------------------------------------------------------------------------- /src/transactions/project/allocate-staking.ts: -------------------------------------------------------------------------------- 1 | import { Lucid, Script, UTxO } from "lucid-cardano"; 2 | 3 | import { PROJECT_AT_TOKEN_NAMES } from "@/contracts/common/constants"; 4 | import { addressFromScriptHashes } from "@/helpers/lucid"; 5 | import * as S from "@/schema"; 6 | import { 7 | ProjectDatum, 8 | ProjectMintingRedeemer, 9 | ProjectRedeemer, 10 | ProjectScriptDatum, 11 | } from "@/schema/teiki/project"; 12 | import { ProtocolParamsDatum } from "@/schema/teiki/protocol"; 13 | import { assert } from "@/utils"; 14 | 15 | import { PROJECT_SCRIPT_UTXO_ADA } from "../constants"; 16 | 17 | export type ProjectInfo = { 18 | projectUtxo: UTxO; 19 | newStakeValidator: Script; 20 | }; 21 | 22 | export type Params = { 23 | protocolParamsUtxo: UTxO; 24 | projectInfoList: ProjectInfo[]; 25 | projectVRefScriptUtxo: UTxO; 26 | projectAtMpRefScriptUtxo: UTxO; 27 | }; 28 | 29 | export function allocateStakingTx( 30 | lucid: Lucid, 31 | { 32 | protocolParamsUtxo, 33 | projectInfoList, 34 | projectVRefScriptUtxo, 35 | projectAtMpRefScriptUtxo, 36 | }: Params 37 | ) { 38 | assert( 39 | protocolParamsUtxo.datum != null, 40 | "Invalid protocol params UTxO: Missing inline datum" 41 | ); 42 | const protocolParams = S.fromData( 43 | S.fromCbor(protocolParamsUtxo.datum), 44 | ProtocolParamsDatum 45 | ); 46 | 47 | assert( 48 | projectAtMpRefScriptUtxo.scriptRef != null, 49 | "Invalid project AT minting policy reference UTxO: must reference project AT minting policy script" 50 | ); 51 | 52 | const projectAtMph = lucid.utils.validatorToScriptHash( 53 | projectAtMpRefScriptUtxo.scriptRef 54 | ); 55 | const projectScriptAtUnit = 56 | projectAtMph + PROJECT_AT_TOKEN_NAMES.PROJECT_SCRIPT; 57 | const projectMintingRedeemer: ProjectMintingRedeemer = { 58 | case: "AllocateStaking", 59 | }; 60 | 61 | let tx = lucid 62 | .newTx() 63 | .readFrom([ 64 | protocolParamsUtxo, 65 | projectVRefScriptUtxo, 66 | projectAtMpRefScriptUtxo, 67 | ]) 68 | .mintAssets( 69 | { [projectScriptAtUnit]: BigInt(projectInfoList.length) }, 70 | S.toCbor(S.toData(projectMintingRedeemer, ProjectMintingRedeemer)) 71 | ); 72 | 73 | for (const projectInfo of projectInfoList) { 74 | const projectUtxo = projectInfo.projectUtxo; 75 | assert( 76 | projectUtxo.datum != null, 77 | "Invalid project UTxO: Missing inline datum" 78 | ); 79 | 80 | const projectDatum = S.fromData( 81 | S.fromCbor(projectUtxo.datum), 82 | ProjectDatum 83 | ); 84 | 85 | const newStakingValidatorHash = lucid.utils.validatorToScriptHash( 86 | projectInfo.newStakeValidator 87 | ); 88 | const projectScriptUtxoAddress = addressFromScriptHashes( 89 | lucid, 90 | protocolParams.registry.projectScriptValidator.latest.script.hash, 91 | newStakingValidatorHash 92 | ); 93 | 94 | const projectScriptDatum: ProjectScriptDatum = { 95 | projectId: projectDatum.projectId, 96 | stakingKeyDeposit: protocolParams.stakeKeyDeposit, 97 | }; 98 | 99 | const projectRedeemer: ProjectRedeemer = { 100 | case: "AllocateStakingValidator", 101 | newStakingValidator: { 102 | script: { 103 | hash: newStakingValidatorHash, 104 | }, 105 | }, 106 | }; 107 | 108 | const newStakeCredential = lucid.utils.scriptHashToCredential( 109 | lucid.utils.validatorToScriptHash(projectInfo.newStakeValidator) 110 | ); 111 | 112 | const newProjectStakeAddress = 113 | lucid.utils.credentialToRewardAddress(newStakeCredential); 114 | 115 | tx = tx 116 | .collectFrom( 117 | [projectUtxo], 118 | S.toCbor(S.toData(projectRedeemer, ProjectRedeemer)) 119 | ) 120 | .payToContract( 121 | projectUtxo.address, 122 | { inline: projectUtxo.datum }, 123 | { 124 | ...projectUtxo.assets, 125 | lovelace: 126 | BigInt(projectUtxo.assets.lovelace) - 127 | protocolParams.stakeKeyDeposit - 128 | PROJECT_SCRIPT_UTXO_ADA, 129 | } 130 | ) 131 | .payToContract( 132 | projectScriptUtxoAddress, 133 | { 134 | inline: S.toCbor(S.toData(projectScriptDatum, ProjectScriptDatum)), 135 | scriptRef: projectInfo.newStakeValidator, 136 | }, 137 | { 138 | lovelace: PROJECT_SCRIPT_UTXO_ADA, 139 | [projectScriptAtUnit]: 1n, 140 | } 141 | ) 142 | .registerStake(newProjectStakeAddress); 143 | } 144 | 145 | return tx; 146 | } 147 | -------------------------------------------------------------------------------- /src/contracts/common/helpers.ts: -------------------------------------------------------------------------------- 1 | import { header, helios, module } from "../program"; 2 | 3 | export default helios` 4 | ${header("module", "helpers")} 5 | 6 | import { 7 | RATIO_MULTIPLIER, 8 | PROTOCOL_PARAMS_NFT_TOKEN_NAME 9 | } from ${module("constants")} 10 | 11 | import { Datum as PParamsDatum } 12 | from ${module("v__protocol_params__types")} 13 | 14 | import { 15 | TokenPredicate, 16 | MintingPredicate, 17 | MintingRedeemer 18 | } from ${module("v__teiki_plant__types")} 19 | 20 | func is_tx_authorized_by(tx: Tx, credential: Credential) -> Bool{ 21 | credential.switch { 22 | pubKey: PubKey => { 23 | tx.is_signed_by(pubKey.hash) 24 | }, 25 | else => { 26 | tx.inputs.any( 27 | (input: TxInput) -> { 28 | input.output.address.credential == credential 29 | } 30 | ) 31 | } 32 | } 33 | } 34 | 35 | func does_tx_pass_token_preciate_check(tx: Tx, predicate: TokenPredicate) -> Bool { 36 | mph: MintingPolicyHash = predicate.minting_policy_hash; 37 | 38 | predicate.token_names.switch { 39 | None => tx.inputs.any( 40 | (input: TxInput) -> { 41 | input.output.value.contains_policy(mph) 42 | } 43 | ), 44 | else => { 45 | token_names: []ByteArray = predicate.token_names.unwrap(); 46 | 47 | token_names.all( 48 | (token_name: ByteArray) -> { 49 | tx.inputs.any( 50 | (input: TxInput) -> { 51 | input.output.value.get_safe(AssetClass::new(mph, token_name)) > 0 52 | } 53 | ) 54 | } 55 | ) 56 | } 57 | } 58 | } 59 | 60 | func does_tx_pass_minting_preciate_check(tx: Tx, predicate: MintingPredicate) -> Bool { 61 | mph: MintingPolicyHash = predicate.minting_policy_hash; 62 | minting_redeemer: Data = 63 | tx.redeemers.get(ScriptPurpose::new_minting(mph)); 64 | 65 | predicate.redeemer.switch { 66 | Any => true, 67 | constr_in: ConstrIn => { 68 | minting_constr_tag: Int = minting_redeemer.tag; 69 | constr_in.constrs.any( 70 | (constr: Int) -> { constr == minting_constr_tag } 71 | ) 72 | }, 73 | constr_not_in: ConstrNotIn => { 74 | minting_constr_tag: Int = minting_redeemer.tag; 75 | constr_not_in.constrs.all( 76 | (constr: Int) -> { constr != minting_constr_tag } 77 | ) 78 | } 79 | } 80 | } 81 | 82 | // TODO: PROTOCOL_NFT_MPH should be a global param 83 | func find_pparams_datum_from_inputs( 84 | inputs: []TxInput, 85 | protocol_nft_mph: MintingPolicyHash 86 | ) -> PParamsDatum { 87 | protocol_params_nft: AssetClass = 88 | AssetClass::new(protocol_nft_mph, PROTOCOL_PARAMS_NFT_TOKEN_NAME); 89 | inputs 90 | .map((input: TxInput) -> { input.output }) 91 | .find((output: TxOutput) -> { output.value.get_safe(protocol_params_nft) == 1 }) 92 | .datum 93 | .switch { 94 | i: Inline => PParamsDatum::from_data(i.data), 95 | else => error("Invalid protocol params UTxO: missing inline datum") 96 | } 97 | } 98 | 99 | // TODO: PROTOCOL_NFT_MPH should be a global param 100 | func find_pparams_datum_from_outputs( 101 | outputs: []TxOutput, 102 | protocol_nft_mph: MintingPolicyHash 103 | ) -> PParamsDatum { 104 | protocol_params_nft: AssetClass = 105 | AssetClass::new(protocol_nft_mph, PROTOCOL_PARAMS_NFT_TOKEN_NAME); 106 | outputs 107 | .find((output: TxOutput) -> { output.value.get_safe(protocol_params_nft) == 1 }) 108 | .datum 109 | .switch { 110 | i: Inline => PParamsDatum::from_data(i.data), 111 | else => error("Invalid protocol params UTxO: missing inline datum") 112 | } 113 | } 114 | 115 | func staking_credential_to_validator_hash(staking_credential: StakingCredential) -> StakingValidatorHash { 116 | staking_credential.switch { 117 | h: Hash => h.hash.switch { 118 | v: Validator => v.hash, 119 | else => error("not StakingValidatorHash")}, 120 | else => error("not StakingHash") 121 | } 122 | } 123 | 124 | func script_hash_to_staking_credential(script_hash: ScriptHash) -> StakingCredential { 125 | StakingCredential::new_hash( 126 | StakingHash::new_validator( 127 | StakingValidatorHash::from_script_hash(script_hash) 128 | ) 129 | ) 130 | } 131 | 132 | func min(a: Int, b: Int) -> Int { 133 | if (a < b) { 134 | a 135 | } else { 136 | b 137 | } 138 | } 139 | 140 | func max(a: Int, b: Int) -> Int { 141 | if (a > b) { 142 | a 143 | } else { 144 | b 145 | } 146 | } 147 | `; 148 | -------------------------------------------------------------------------------- /src/contracts/protocol/protocol-proposal.v/main.ts: -------------------------------------------------------------------------------- 1 | import { Hex } from "@/types"; 2 | 3 | import { header, helios, HeliosScript, module } from "../../program"; 4 | 5 | export type Params = { 6 | protocolNftMph: Hex; 7 | }; 8 | 9 | export default function main({ protocolNftMph }: Params): HeliosScript { 10 | return helios` 11 | ${header("spending", "v__protocol_proposal")} 12 | 13 | import { 14 | ADA_MINTING_POLICY_HASH, 15 | PROTOCOL_PARAMS_NFT_TOKEN_NAME, 16 | PROTOCOL_PROPOSAL_NFT_TOKEN_NAME 17 | } from ${module("constants")} 18 | 19 | import { is_tx_authorized_by } 20 | from ${module("helpers")} 21 | 22 | import { Datum as PParamsDatum } 23 | from ${module("v__protocol_params__types")} 24 | 25 | import { Datum, Redeemer, Proposal } 26 | from ${module("v__protocol_proposal__types")} 27 | 28 | const PROTOCOL_NFT_MPH: MintingPolicyHash = 29 | MintingPolicyHash::new(#${protocolNftMph}) 30 | 31 | const PROTOCOL_PARAMS_NFT: AssetClass = 32 | AssetClass::new(PROTOCOL_NFT_MPH, PROTOCOL_PARAMS_NFT_TOKEN_NAME) 33 | 34 | func are_output_value_and_address_valid( 35 | output: TxOutput, 36 | address: Address, 37 | token_name: ByteArray 38 | ) -> Bool { 39 | output.value.to_map().all( 40 | (mph: MintingPolicyHash, tokens: Map[ByteArray]Int) -> { 41 | if (mph == PROTOCOL_NFT_MPH) { 42 | tokens == Map[ByteArray]Int {token_name: 1} 43 | } else { 44 | mph == ADA_MINTING_POLICY_HASH 45 | } 46 | } 47 | ) 48 | && output.address == address 49 | } 50 | 51 | func is_proposal_empty(datum: Datum) -> Bool { 52 | datum.proposal.switch { 53 | None => true, 54 | else => false 55 | } 56 | } 57 | 58 | func main(datum: Datum, redeemer: Redeemer, ctx: ScriptContext) -> Bool { 59 | tx: Tx = ctx.tx; 60 | 61 | own_spending_output: TxOutput = ctx.get_current_input().output; 62 | 63 | own_producing_output: TxOutput = 64 | tx.outputs_locked_by(ctx.get_current_validator_hash()).head; 65 | 66 | own_output_datum: Datum = 67 | own_producing_output.datum.switch { 68 | i: Inline => Datum::from_data(i.data), 69 | else => error("Invalid proposal UTxO: missing inline datum") 70 | }; 71 | 72 | pparams_input: TxInput = 73 | (tx.inputs + tx.ref_inputs) 74 | .find( 75 | (input: TxInput) -> { input.output.value.get_safe(PROTOCOL_PARAMS_NFT) == 1 } 76 | ); 77 | 78 | pparams_datum: PParamsDatum = 79 | pparams_input.output.datum.switch { 80 | i: Inline => PParamsDatum::from_data(i.data), 81 | else => error("Invalid protocol params UTxO: missing inline datum") 82 | }; 83 | 84 | assert( 85 | is_tx_authorized_by(tx, pparams_datum.governor_address.credential), 86 | "The proposal must be authorized" 87 | ); 88 | 89 | assert( 90 | are_output_value_and_address_valid( 91 | own_producing_output, 92 | own_spending_output.address, 93 | PROTOCOL_PROPOSAL_NFT_TOKEN_NAME 94 | ), 95 | "Invalid proposal output value and address" 96 | ); 97 | 98 | redeemer.switch { 99 | 100 | Propose => { 101 | new_proposal: Proposal = own_output_datum.proposal.unwrap(); 102 | 103 | new_proposal.in_effect_at >= tx.time_range.end + pparams_datum.proposal_waiting_period 104 | && new_proposal.base == pparams_input.output_id 105 | }, 106 | 107 | Apply => { 108 | proposal: Proposal = datum.proposal.unwrap(); 109 | 110 | new_pparams_output: TxOutput = 111 | tx.outputs 112 | .find((output: TxOutput) -> { output.value.get_safe(PROTOCOL_PARAMS_NFT) == 1 }); 113 | 114 | new_pparams_datum: PParamsDatum = 115 | new_pparams_output.datum.switch { 116 | i: Inline => PParamsDatum::from_data(i.data), 117 | else => error("Invalid protocol params UTxO: missing inline datum") 118 | }; 119 | 120 | are_output_value_and_address_valid( 121 | new_pparams_output, 122 | pparams_input.output.address, 123 | PROTOCOL_PARAMS_NFT_TOKEN_NAME 124 | ) 125 | && tx.time_range.start >= proposal.in_effect_at 126 | && proposal.base == pparams_input.output_id 127 | && proposal.params == new_pparams_datum 128 | && is_proposal_empty(own_output_datum) 129 | }, 130 | 131 | Cancel => { 132 | is_proposal_empty(own_output_datum) && !is_proposal_empty(datum) 133 | } 134 | 135 | } 136 | } 137 | `; 138 | } 139 | -------------------------------------------------------------------------------- /src/contracts/meta-protocol/teiki-plant.v/main.ts: -------------------------------------------------------------------------------- 1 | import { Hex } from "@/types"; 2 | 3 | import { HeliosScript, helios, header, module } from "../../program"; 4 | 5 | export type Params = { 6 | teikiPlantNftMph: Hex; 7 | }; 8 | 9 | export default function main({ teikiPlantNftMph }: Params): HeliosScript { 10 | return helios` 11 | ${header("spending", "v__teiki_plant")} 12 | 13 | import { 14 | ADA_MINTING_POLICY_HASH, 15 | TEIKI_PLANT_NFT_TOKEN_NAME 16 | } from ${module("constants")} 17 | 18 | import { 19 | is_tx_authorized_by, 20 | does_tx_pass_token_preciate_check, 21 | does_tx_pass_minting_preciate_check 22 | } from ${module("helpers")} 23 | 24 | import { 25 | Datum, 26 | Redeemer, 27 | Authorization, 28 | TokenPredicate, 29 | MintingPredicate, 30 | MintingRedeemer, 31 | RulesProposal 32 | } from ${module("v__teiki_plant__types")} 33 | 34 | const TEIKI_PLANT_NFT_MPH: MintingPolicyHash = 35 | MintingPolicyHash::new(#${teikiPlantNftMph}) 36 | 37 | func main(datum: Datum, redeemer: Redeemer, ctx: ScriptContext) -> Bool { 38 | tx: Tx = ctx.tx; 39 | own_spending_input: TxInput = ctx.get_current_input(); 40 | own_validator_hash: ValidatorHash = ctx.get_current_validator_hash(); 41 | 42 | produced_output: TxOutput = tx.outputs_locked_by(own_validator_hash).head; 43 | produced_output_datum: Datum = 44 | produced_output.datum.switch { 45 | i: Inline => Datum::from_data(i.data), 46 | else => error("Invalid Teiki Plant UTxO: Missing inline datum") 47 | }; 48 | 49 | proposal_authorizations: []Authorization = datum.rules.proposal_authorizations; 50 | assert( 51 | proposal_authorizations.any( 52 | (authorization: Authorization) -> { 53 | authorization.switch { 54 | must_be: MustBe => { 55 | is_tx_authorized_by(tx, must_be.credential) 56 | }, 57 | must_have: MustHave => { 58 | does_tx_pass_token_preciate_check(tx, must_have.predicate) 59 | }, 60 | must_mint: MustMint => { 61 | does_tx_pass_minting_preciate_check(tx, must_mint.predicate) 62 | } 63 | } 64 | } 65 | ), 66 | "The proposal must be authorized" 67 | ); 68 | 69 | is_teiki_plant_value_preserved: Bool = 70 | produced_output.value.to_map().all( 71 | (mph: MintingPolicyHash, tokens: Map[ByteArray]Int) -> { 72 | if (mph == TEIKI_PLANT_NFT_MPH) { 73 | tokens == Map[ByteArray]Int {TEIKI_PLANT_NFT_TOKEN_NAME: 1} 74 | } else { 75 | mph == ADA_MINTING_POLICY_HASH 76 | } 77 | } 78 | ); 79 | assert( 80 | is_teiki_plant_value_preserved, 81 | "Teiki Plant UTxO Value must be preserved" 82 | ); 83 | 84 | redeemer.switch { 85 | 86 | Propose => { 87 | assert( 88 | produced_output.address == own_spending_input.output.address, 89 | "Teiki Plant UTxO Address must be preserved" 90 | ); 91 | 92 | output_proposal: RulesProposal = produced_output_datum.proposal.unwrap(); 93 | 94 | produced_output_datum.rules == datum.rules 95 | && output_proposal.in_effect_at 96 | >= tx.time_range.end + datum.rules.proposal_waiting_period 97 | }, 98 | 99 | Apply => { 100 | input_proposal: RulesProposal = datum.proposal.unwrap(); 101 | 102 | assert( 103 | produced_output.address.credential == Credential::new_validator(own_validator_hash), 104 | "Teiki Plant UTxO Address Payment Credential must be preserved" 105 | ); 106 | 107 | tx.time_range.start >= input_proposal.in_effect_at 108 | && produced_output_datum.rules == input_proposal.rules 109 | && produced_output_datum.proposal.switch { 110 | None => true, 111 | else => false 112 | } 113 | }, 114 | 115 | Cancel => { 116 | assert( 117 | datum.proposal.switch { 118 | Some => true, 119 | else => false 120 | }, 121 | "Input proposal must not be None" 122 | ); 123 | 124 | assert( 125 | produced_output.address == own_spending_input.output.address, 126 | "Teiki Plant UTxO Address must be preserved" 127 | ); 128 | 129 | produced_output_datum.rules == datum.rules 130 | && produced_output_datum.proposal.switch { 131 | None => true, 132 | else => false 133 | } 134 | } 135 | 136 | } 137 | } 138 | `; 139 | } 140 | -------------------------------------------------------------------------------- /tests/emulator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | Assets, 4 | Datum, 5 | DatumHash, 6 | Emulator, 7 | generateSeedPhrase, 8 | KeyHash, 9 | Lucid, 10 | OutRef, 11 | ScriptHash, 12 | toHex, 13 | UTxO, 14 | } from "lucid-cardano"; 15 | 16 | import { addressFromScriptHashes } from "@/helpers/lucid"; 17 | import { Kolour, Referral } from "@/schema/teiki/kolours"; 18 | import { Hex } from "@/types"; 19 | import { assert } from "@/utils"; 20 | 21 | type Crypto = { 22 | getRandomValues(array: T): T; 23 | }; 24 | 25 | function loadCrypto(): Crypto { 26 | // global.crypto is loaded by Lucid 27 | const crypto = (global as unknown as Record<"crypto", Crypto | undefined>) 28 | .crypto; 29 | assert(crypto, "global.crypto is not loaded"); 30 | return crypto; 31 | } 32 | 33 | async function headlessLucid(): Promise { 34 | return Lucid.new(undefined, "Custom"); 35 | } 36 | 37 | const DEFAULT_LOVELACE_PER_ACCOUNT = 1_000_000_000_000n; 38 | 39 | // https://github.com/spacebudz/nebula/blob/main/contract/tests/mod.test.ts#L17 40 | export async function generateAccount(assets?: Assets) { 41 | const lucid = await headlessLucid(); 42 | const seedPhrase = generateSeedPhrase(); 43 | 44 | const lovelace = assets?.lovelace ?? DEFAULT_LOVELACE_PER_ACCOUNT; 45 | return { 46 | seedPhrase, 47 | address: await lucid.selectWalletFromSeed(seedPhrase).wallet.address(), 48 | assets: { ...assets, lovelace }, 49 | }; 50 | } 51 | 52 | export function generateOutRef(): OutRef { 53 | // 32 bytes for txHash, 1 byte for outputIndex 54 | const bytes = loadCrypto().getRandomValues(new Uint8Array(33)); 55 | const txHash = toHex(bytes.slice(0, 32)); 56 | const outputIndex = bytes[32]; 57 | return { txHash, outputIndex }; 58 | } 59 | 60 | function toLedgerFlatRef(ref: OutRef): string { 61 | return ref.txHash + ref.outputIndex; 62 | } 63 | 64 | export function attachUtxos(emulator: Emulator, utxos: UTxO[]): Emulator { 65 | emulator.ledger = { 66 | ...emulator.ledger, 67 | ...Object.fromEntries( 68 | utxos.map((utxo) => [toLedgerFlatRef(utxo), { utxo, spent: false }]) 69 | ), 70 | }; 71 | return emulator; 72 | } 73 | 74 | export function detachUtxos(emulator: Emulator, refs: OutRef[]): Emulator { 75 | const toDetach = new Set(refs.map(toLedgerFlatRef)); 76 | emulator.ledger = Object.fromEntries( 77 | Object.entries(emulator.ledger).filter( 78 | ([flatRef, _]) => !toDetach.has(flatRef) 79 | ) 80 | ); 81 | return emulator; 82 | } 83 | 84 | export function spendUtxos(emulator: Emulator, refs: OutRef[]): Emulator { 85 | const toSpend = new Set(refs.map(toLedgerFlatRef)); 86 | emulator.ledger = Object.fromEntries( 87 | Object.entries(emulator.ledger).map(([flatRef, { utxo, spent }]) => [ 88 | flatRef, 89 | { utxo, spent: spent || toSpend.has(flatRef) }, 90 | ]) 91 | ); 92 | return emulator; 93 | } 94 | 95 | export function unspendUtxos(emulator: Emulator, refs: OutRef[]): Emulator { 96 | const toUnspend = new Set(refs.map(toLedgerFlatRef)); 97 | emulator.ledger = Object.fromEntries( 98 | Object.entries(emulator.ledger).map(([flatRef, { utxo, spent }]) => [ 99 | flatRef, 100 | { utxo, spent: spent && !toUnspend.has(flatRef) }, 101 | ]) 102 | ); 103 | return emulator; 104 | } 105 | 106 | export function attachDatums( 107 | emulator: Emulator, 108 | datums: Record 109 | ): Emulator { 110 | emulator.datumTable = { ...emulator.datumTable, ...datums }; 111 | return emulator; 112 | } 113 | 114 | export function generateBlake2b224Hash(): KeyHash | ScriptHash { 115 | return toHex(loadCrypto().getRandomValues(new Uint8Array(28))); 116 | } 117 | 118 | export function generateBlake2b256Hash(): KeyHash | ScriptHash { 119 | return toHex(loadCrypto().getRandomValues(new Uint8Array(32))); 120 | } 121 | 122 | export function generateStakingSeed(): Hex { 123 | return toHex(loadCrypto().getRandomValues(new Uint8Array(32))); 124 | } 125 | 126 | export function generateScriptAddress(lucid: Lucid): Address { 127 | return addressFromScriptHashes(lucid, generateBlake2b224Hash()); 128 | } 129 | 130 | export function generateWalletAddress(lucid: Lucid): Address { 131 | return lucid.utils.credentialToAddress( 132 | lucid.utils.keyHashToCredential(generateBlake2b224Hash()) 133 | ); 134 | } 135 | 136 | export function generateKolour(): Kolour { 137 | return toHex(loadCrypto().getRandomValues(new Uint8Array(3))); 138 | } 139 | 140 | export function generateCid(): Hex { 141 | return "Qm" + toHex(loadCrypto().getRandomValues(new Uint8Array(22))); 142 | } 143 | 144 | export function generateReferral(): Referral { 145 | return { 146 | id: toHex(loadCrypto().getRandomValues(new Uint8Array(32))), 147 | discount: 0, 148 | }; 149 | } 150 | 151 | export function generateDiscount(): number { 152 | return 5000; // 50% discount, 153 | } 154 | -------------------------------------------------------------------------------- /src/transactions/backing/claim-rewards-by-flower.ts: -------------------------------------------------------------------------------- 1 | import { Assets, Lucid, UTxO } from "lucid-cardano"; 2 | 3 | import { constructPlantHashUsingBlake2b } from "@/helpers/schema"; 4 | import * as S from "@/schema"; 5 | import { Plant, ProofOfBackingMintingRedeemer } from "@/schema/teiki/backing"; 6 | import { ProjectDatum } from "@/schema/teiki/project"; 7 | import { ProtocolParamsDatum } from "@/schema/teiki/protocol"; 8 | import { Hex, TimeDifference, UnixTime } from "@/types"; 9 | import { assert } from "@/utils"; 10 | 11 | import { attachTeikiNftMetadata } from "../meta-data"; 12 | 13 | import { mintTeiki, MintTeikiParams } from "./utils"; 14 | 15 | export type Params = { 16 | protocolParamsUtxo: UTxO; 17 | projectUtxo: UTxO; // project status is not `PreDelisted` 18 | backingInfo: BackingInfo; 19 | teikiMintingInfo: TeikiMintingInfo; 20 | txTime: UnixTime; 21 | txTtl?: TimeDifference; 22 | }; 23 | 24 | export type BackingInfo = { 25 | // sorted by backing TxOutputId 26 | // and milestone backed does not reach the current project milestone 27 | flowers: Plant[]; 28 | proofOfBackingMpRefUtxo: UTxO; 29 | proofOfBackingMph: Hex; 30 | }; 31 | 32 | export type TeikiMintingInfo = { 33 | teikiMph: Hex; 34 | teikiMpRefUtxo: UTxO; 35 | teikiPlantVRefUtxo: UTxO; 36 | sharedTreasuryVRefUtxo: UTxO; 37 | sharedTreasuryUtxo: UTxO; 38 | }; 39 | 40 | export function claimRewardsByFlowerTx( 41 | lucid: Lucid, 42 | { 43 | protocolParamsUtxo, 44 | projectUtxo, 45 | backingInfo, 46 | teikiMintingInfo, 47 | txTime, 48 | txTtl = 600_000, 49 | }: Params 50 | ) { 51 | assert( 52 | protocolParamsUtxo.datum != null, 53 | "Invalid protocol params UTxO: Missing inline datum" 54 | ); 55 | 56 | const protocolParams = S.fromData( 57 | S.fromCbor(protocolParamsUtxo.datum), 58 | ProtocolParamsDatum 59 | ); 60 | 61 | const { proofOfBackingMpRefUtxo, flowers } = backingInfo; 62 | 63 | assert( 64 | proofOfBackingMpRefUtxo.scriptRef != null, 65 | "Invalid proof of backing reference UTxO: must reference proof of backing script" 66 | ); 67 | 68 | const proofOfBackingMph = lucid.utils.validatorToScriptHash( 69 | proofOfBackingMpRefUtxo.scriptRef 70 | ); 71 | 72 | const proofOfBackingMintingRedeemer = S.toCbor( 73 | S.toData({ case: "ClaimRewards", flowers }, ProofOfBackingMintingRedeemer) 74 | ); 75 | 76 | assert( 77 | projectUtxo.datum != null, 78 | "Invalid project UTxO: Missing inline datum" 79 | ); 80 | 81 | const projectDatum = S.fromData(S.fromCbor(projectUtxo.datum), ProjectDatum); 82 | 83 | assert( 84 | projectDatum.status.type != "PreDelisted", 85 | "Unable to claim rewards when project status is pre-delisted" 86 | ); 87 | 88 | const projectId = projectDatum.projectId.id; 89 | const currentProjectMilestone = projectDatum.milestoneReached; 90 | 91 | const txTimeStart = txTime; 92 | const txTimeEnd = txTime + txTtl; 93 | 94 | let tx = lucid 95 | .newTx() 96 | .readFrom([proofOfBackingMpRefUtxo, projectUtxo, protocolParamsUtxo]) 97 | .validFrom(txTimeStart) 98 | .validTo(txTimeEnd); 99 | 100 | let mintingPlantAssets: Assets = {}; 101 | let totalTeikiRewards = 0n; 102 | for (const flower of flowers) { 103 | assert(flower.isMatured === false, "Unable to claim rewards by fruit"); 104 | 105 | assert(flower.projectId.id === projectId, "Incorrect flower project id"); 106 | 107 | assert( 108 | flower.milestoneBacked < currentProjectMilestone, 109 | "Invalid flower milestone backed" 110 | ); 111 | 112 | const fruit: Plant = { 113 | ...flower, 114 | isMatured: true, 115 | }; 116 | 117 | const fruitPlantHash = constructPlantHashUsingBlake2b(fruit); 118 | 119 | mintingPlantAssets = { 120 | ...mintingPlantAssets, 121 | [proofOfBackingMph + constructPlantHashUsingBlake2b(flower)]: -1n, 122 | [proofOfBackingMph + fruitPlantHash]: 1n, 123 | }; 124 | 125 | const backingDuration = BigInt( 126 | flower.unbackedAt.timestamp - flower.backedAt.timestamp 127 | ); 128 | 129 | const teikiRewards = 130 | (flower.backingAmount * 131 | (backingDuration / BigInt(protocolParams.epochLength.milliseconds))) / 132 | protocolParams.teikiCoefficient; 133 | 134 | totalTeikiRewards += teikiRewards; 135 | 136 | tx = attachTeikiNftMetadata(tx, { 137 | policyId: backingInfo.proofOfBackingMph, 138 | assetName: fruitPlantHash, 139 | nftName: "Teiki Kuda", 140 | projectId: flower.projectId.id, 141 | backingAmount: flower.backingAmount, 142 | duration: backingDuration, 143 | }); 144 | } 145 | 146 | tx = tx.mintAssets(mintingPlantAssets, proofOfBackingMintingRedeemer); 147 | 148 | const mintTeikiParams: MintTeikiParams = { 149 | teikiMintingInfo, 150 | totalTeikiRewards, 151 | protocolParams, 152 | projectDatum, 153 | txTimeStart, 154 | }; 155 | tx = mintTeiki(tx, mintTeikiParams); 156 | 157 | return tx; 158 | } 159 | -------------------------------------------------------------------------------- /tests/schema.test.ts: -------------------------------------------------------------------------------- 1 | import * as helios from "@hyperionbt/helios"; 2 | import { Lucid } from "lucid-cardano"; 3 | 4 | import { asBool } from "@/helpers/helios"; 5 | import { constructAddress, deconstructAddress } from "@/helpers/schema"; 6 | import { fromJson, toJson } from "@/json"; 7 | import * as S from "@/schema"; 8 | import { Hex, OutRef } from "@/types"; 9 | 10 | describe("complex schema", () => { 11 | const OneStruct = S.Struct(S.Inline(S.ByteArray)); 12 | const MiniStruct = S.Struct({ 13 | b: S.Int, 14 | c: S.String, 15 | }); 16 | const InlinedEnum = S.Enum("type", { 17 | Old: S.Inline(MiniStruct), 18 | New: S.Void, 19 | }); 20 | 21 | const Datum = S.Enum("direction", { 22 | Up: S.Void, 23 | Down: S.Void, 24 | Left: { 25 | foo: OneStruct, 26 | bar: S.Option(MiniStruct), 27 | baz: S.TxOutputId, 28 | inl: InlinedEnum, 29 | map: S.Map(S.ValidatorHash, S.Int), 30 | }, 31 | Right: S.Void, 32 | }); 33 | type Datum = S.Static; 34 | 35 | type Params = { a: Hex; b: number; c: string; d: OutRef }; 36 | 37 | function buildHeliosScript({ a, b, c, d }: Params) { 38 | return ` 39 | testing schema 40 | 41 | struct OneStruct { 42 | a: ByteArray 43 | } 44 | 45 | struct MiniStruct { 46 | b: Int 47 | c: String 48 | } 49 | 50 | enum InlinedEnum { 51 | Old { inline: MiniStruct } 52 | New 53 | } 54 | 55 | enum Datum { 56 | Up 57 | Down 58 | Left { 59 | foo: OneStruct 60 | bar: Option[MiniStruct] 61 | baz: TxOutputId 62 | inl: InlinedEnum 63 | map: Map[ValidatorHash]Int 64 | } 65 | Right 66 | } 67 | 68 | func main(datum: Datum) -> Bool { 69 | datum.switch { 70 | i: Left => { 71 | i.foo.a == #${a} 72 | && i.bar.unwrap().b == ${b.toString()} 73 | && i.bar.unwrap().c == "${c}" 74 | && i.baz.tx_id.show() == "${d.txHash}" 75 | && i.baz.index == ${d.outputIndex.toString()} 76 | && i.inl == InlinedEnum::Old { 77 | MiniStruct { 78 | b: ${b.toString()}, 79 | c: "${c}" 80 | } 81 | } 82 | && i.map == Map[ValidatorHash]Int { 83 | ValidatorHash::new(#${a}): ${b.toString()} 84 | } 85 | }, 86 | else => { false } 87 | } 88 | } 89 | `; 90 | } 91 | 92 | function buildDatum({ a, b, c, d }: Params): Datum { 93 | return { 94 | direction: "Left", 95 | foo: a, 96 | bar: { b: BigInt(b), c }, 97 | baz: { 98 | txId: d.txHash, 99 | index: BigInt(d.outputIndex), 100 | }, 101 | inl: { type: "Old", b: BigInt(b), c }, 102 | map: [[{ script: { hash: a } }, BigInt(b)]], 103 | }; 104 | } 105 | 106 | const sampleParams: Params = { 107 | a: "beef1234", 108 | b: 42, 109 | c: "Hello World", 110 | d: { 111 | txHash: 112 | "e1ffe6d8e94556ce6f24e53d94dc5d9559c2cbc8f00dad3737c61cd0d60a91dc", 113 | outputIndex: 10, 114 | }, 115 | }; 116 | 117 | const datum: Datum = buildDatum(sampleParams); 118 | 119 | it("round trip cbor", () => { 120 | const sampleCbor = 121 | "d87b9f44beef1234d8799f9f182a4b48656c6c6f20576f726c64ffffd8799fd8799f5820e1ffe6d8e94556ce6f24e53d94dc5d9559c2cbc8f00dad3737c61cd0d60a91dcff0affd8799f9f182a4b48656c6c6f20576f726c64ffffa144beef1234182aff"; 122 | const cbor = S.toCbor(S.toData(datum, Datum)); 123 | expect(cbor).toBe(sampleCbor); 124 | 125 | const deserialized = S.fromData(S.fromCbor(cbor), Datum); 126 | expect(deserialized).toStrictEqual(datum); 127 | }); 128 | 129 | it("round trip json", () => { 130 | // eslint-disable-next-line jest/prefer-strict-equal 131 | expect(fromJson(toJson(datum), { forceBigInt: true })).toEqual(datum); 132 | }); 133 | 134 | it("helios compatibility", async () => { 135 | expect.assertions(2); 136 | const cbor = S.toCbor(S.toData(datum, Datum)); 137 | const heliosScript = buildHeliosScript(sampleParams); 138 | const uplcProgram = helios.Program.new(heliosScript).compile(true); 139 | const dummy = helios.exportedForTesting.Site.dummy(); 140 | const [result, _] = await uplcProgram.runWithPrint([ 141 | new helios.UplcDataValue( 142 | dummy, 143 | helios.UplcData.fromCbor(helios.hexToBytes(cbor)) 144 | ), 145 | ]); 146 | expect(result).toBeInstanceOf(helios.UplcValue); 147 | expect(asBool(result as helios.UplcValue)).toBe(true); 148 | }); 149 | 150 | it("address", async () => { 151 | expect.assertions(1); 152 | const addresses = [ 153 | "addr_test1qpgjllcnfw42z6ckd2hv2uffs78jac2ky2zyxqvazesgstrdhps0t48pq9ez20jj52p45g7kcz3qegusd9sj6va3px2qnv9r8h", 154 | ]; 155 | const lucid = await Lucid.new(undefined, "Custom"); 156 | for (const address of addresses) { 157 | const reconAddress = deconstructAddress(lucid, constructAddress(address)); 158 | expect(reconAddress).toStrictEqual(address); 159 | } 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /tests/kolour-txs.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable jest/no-conditional-in-test */ 2 | import { Emulator, Lucid, C } from "lucid-cardano"; 3 | 4 | import { compileKolourNftScript } from "@/commands/compile-kolour-scripts"; 5 | import { exportScript } from "@/contracts/compile"; 6 | import { getPaymentKeyHash, signAndSubmit } from "@/helpers/lucid"; 7 | import { Kolour, KolourEntry } from "@/schema/teiki/kolours"; 8 | import { KOLOUR_TX_MAX_DURATION } from "@/transactions/kolours/constants"; 9 | import { 10 | MintKolourNftTxParams, 11 | buildBurnKolourNftTx, 12 | buildMintKolourNftTx, 13 | verifyKolourNftMintingTx, 14 | } from "@/transactions/kolours/kolour-nft"; 15 | import { Hex } from "@/types"; 16 | 17 | import { 18 | attachUtxos, 19 | generateAccount, 20 | generateCid, 21 | generateDiscount, 22 | generateKolour, 23 | generateOutRef, 24 | generateReferral, 25 | generateScriptAddress, 26 | } from "./emulator"; 27 | 28 | const PRODUCER_ACCOUNT = await generateAccount(); 29 | const emulator = new Emulator([PRODUCER_ACCOUNT]); 30 | const lucid = await Lucid.new(emulator); 31 | 32 | describe("kolour transactions", () => { 33 | it("mint kolour nft tx", async () => { 34 | expect.assertions(1); 35 | 36 | lucid.selectWalletFromSeed(PRODUCER_ACCOUNT.seedPhrase); 37 | const producerAddress = PRODUCER_ACCOUNT.address; 38 | 39 | const kolourNftScript = exportScript( 40 | compileKolourNftScript({ 41 | producerPkh: getPaymentKeyHash(producerAddress), 42 | }) 43 | ); 44 | 45 | const kolourNftRefScriptUtxo = { 46 | ...generateOutRef(), 47 | address: generateScriptAddress(lucid), 48 | assets: { lovelace: 2_000_000n }, 49 | scriptRef: kolourNftScript, 50 | }; 51 | 52 | attachUtxos(emulator, [kolourNftRefScriptUtxo]); 53 | emulator.awaitBlock(20); 54 | 55 | const txTimeStart = emulator.now(); 56 | const txTimeEnd = txTimeStart + 300_000; 57 | 58 | const params: MintKolourNftTxParams = { 59 | quotation: { 60 | source: { 61 | type: "genesis_kreation", 62 | kreation: "Genesis Kreation #00", 63 | }, 64 | baseDiscount: generateDiscount(), 65 | referral: generateReferral(), 66 | kolours: generateKolourListings(1), 67 | userAddress: producerAddress, 68 | feeAddress: producerAddress, 69 | expiration: emulator.now() + Number(KOLOUR_TX_MAX_DURATION), 70 | }, 71 | kolourNftRefScriptUtxo, 72 | producerPkh: getPaymentKeyHash(producerAddress), 73 | txTimeStart, 74 | txTimeEnd, 75 | }; 76 | 77 | const tx = buildMintKolourNftTx(lucid, params) // 78 | .addSigner(producerAddress); 79 | 80 | const txComplete = await tx.complete(); 81 | 82 | const signedTx = await txComplete.sign().complete(); 83 | 84 | verifyKolourNftMintingTx(lucid, { 85 | tx: C.Transaction.from_bytes(Buffer.from(signedTx.toString(), "hex")), 86 | quotation: params.quotation, 87 | kolourNftMph: lucid.utils.validatorToScriptHash(kolourNftScript), 88 | }); 89 | 90 | const txHash = await signedTx.submit(); 91 | await expect(lucid.awaitTx(txHash)).resolves.toBe(true); 92 | }); 93 | 94 | it("burn kolour nft tx", async () => { 95 | expect.assertions(1); 96 | 97 | lucid.selectWalletFromSeed(PRODUCER_ACCOUNT.seedPhrase); 98 | const producerAddress = PRODUCER_ACCOUNT.address; 99 | 100 | const kolourNftScript = exportScript( 101 | compileKolourNftScript({ 102 | producerPkh: getPaymentKeyHash(producerAddress), 103 | }) 104 | ); 105 | const kolourNftMph = lucid.utils.validatorToScriptHash(kolourNftScript); 106 | 107 | const kolours = generateKolours(10); 108 | 109 | const kolourNftRefScriptUtxo = { 110 | ...generateOutRef(), 111 | address: generateScriptAddress(lucid), 112 | assets: { lovelace: 2_000_000n }, 113 | scriptRef: kolourNftScript, 114 | }; 115 | 116 | for (const kolour of kolours) { 117 | const randomKolourNftUtxo = { 118 | ...generateOutRef(), 119 | address: producerAddress, // any address contains kolour NFT 120 | assets: { lovelace: 2_000_000n, [kolourNftMph + kolour]: 1n }, 121 | }; 122 | attachUtxos(emulator, [randomKolourNftUtxo]); 123 | } 124 | 125 | attachUtxos(emulator, [kolourNftRefScriptUtxo]); 126 | emulator.awaitBlock(20); 127 | 128 | const tx = buildBurnKolourNftTx(lucid, { 129 | kolours, 130 | kolourNftRefScriptUtxo, 131 | }) // 132 | .addSigner(producerAddress); 133 | 134 | const txComplete = await tx.complete(); 135 | 136 | await expect(lucid.awaitTx(await signAndSubmit(txComplete))).resolves.toBe( 137 | true 138 | ); 139 | }); 140 | }); 141 | 142 | function generateKolourListings(size: number): Record { 143 | const kolourListings = new Map(); 144 | for (let i = 0; i < size; i++) { 145 | kolourListings.set("aabbcc", { 146 | fee: 2_561_000, 147 | image: "ipfs://" + generateCid(), 148 | }); 149 | } 150 | return Object.fromEntries(kolourListings); 151 | } 152 | 153 | function generateKolours(size: number): Hex[] { 154 | return [...Array(size)].map((_) => generateKolour()); 155 | } 156 | -------------------------------------------------------------------------------- /src/transactions/project/finalize-delist.ts: -------------------------------------------------------------------------------- 1 | import { getAddressDetails, Lucid, UTxO } from "lucid-cardano"; 2 | 3 | import * as S from "@/schema"; 4 | import { 5 | ProjectDatum, 6 | ProjectDetailRedeemer, 7 | ProjectRedeemer, 8 | } from "@/schema/teiki/project"; 9 | import { ProtocolParamsDatum } from "@/schema/teiki/protocol"; 10 | import { OpenTreasuryDatum } from "@/schema/teiki/treasury"; 11 | import { UnixTime } from "@/types"; 12 | import { assert } from "@/utils"; 13 | 14 | import { 15 | INACTIVE_PROJECT_UTXO_ADA, 16 | PROJECT_DELIST_DISCOUNT_CENTS, 17 | RATIO_MULTIPLIER, 18 | } from "../constants"; 19 | 20 | export type ProjectUtxoInfo = { 21 | projectUtxo: UTxO; 22 | projectDetailUtxo: UTxO; 23 | }; 24 | 25 | export type FinalizeDelistParams = { 26 | projectUtxoInfo: ProjectUtxoInfo[]; 27 | projectVRefScriptUtxo: UTxO; 28 | projectDetailVRefScriptUtxo: UTxO; 29 | protocolParamsUtxo: UTxO; 30 | txTime: UnixTime; 31 | }; 32 | 33 | export function finalizeDelistTx( 34 | lucid: Lucid, 35 | { 36 | projectUtxoInfo, 37 | projectVRefScriptUtxo, 38 | projectDetailVRefScriptUtxo, 39 | protocolParamsUtxo, 40 | txTime, 41 | }: FinalizeDelistParams 42 | ) { 43 | assert( 44 | protocolParamsUtxo.datum != null, 45 | "Invalid protocol params UTxO: Missing inline datum" 46 | ); 47 | 48 | const protocolParams = S.fromData( 49 | S.fromCbor(protocolParamsUtxo.datum), 50 | ProtocolParamsDatum 51 | ); 52 | 53 | const txTimeStart = txTime; 54 | 55 | let tx = lucid 56 | .newTx() 57 | .readFrom([ 58 | projectVRefScriptUtxo, 59 | projectDetailVRefScriptUtxo, 60 | protocolParamsUtxo, 61 | ]) 62 | .validFrom(txTimeStart); 63 | 64 | const protocolStakeCredential = lucid.utils.scriptHashToCredential( 65 | protocolParams.registry.protocolStakingValidator.script.hash 66 | ); 67 | 68 | const openTreasuryCredential = lucid.utils.scriptHashToCredential( 69 | protocolParams.registry.openTreasuryValidator.latest.script.hash 70 | ); 71 | 72 | for (const { projectUtxo, projectDetailUtxo } of projectUtxoInfo) { 73 | assert( 74 | projectUtxo.datum != null, 75 | "Invalid project UTxO: Missing inline datum" 76 | ); 77 | assert( 78 | projectDetailUtxo.datum != null, 79 | "Invalid project detail UTxO: Missing inline datum" 80 | ); 81 | 82 | const projectDatum = S.fromData( 83 | S.fromCbor(projectUtxo.datum), 84 | ProjectDatum 85 | ); 86 | 87 | const outputProjectDatum: ProjectDatum = { 88 | ...projectDatum, 89 | status: { 90 | type: "Delisted", 91 | delistedAt: { 92 | timestamp: BigInt(txTimeStart), 93 | }, 94 | }, 95 | }; 96 | 97 | const projectCredential = getAddressDetails( 98 | projectUtxo.address 99 | ).paymentCredential; 100 | assert( 101 | projectCredential, 102 | "Cannot extract payment credential from the project address" 103 | ); 104 | 105 | const outputProjectAddress = lucid.utils.credentialToAddress( 106 | projectCredential, 107 | protocolStakeCredential 108 | ); 109 | 110 | const projectDetailCredential = getAddressDetails( 111 | projectDetailUtxo.address 112 | ).paymentCredential; 113 | assert( 114 | projectDetailCredential, 115 | "Cannot extract payment credential from the project detail address" 116 | ); 117 | 118 | const outputProjectDetailAddress = lucid.utils.credentialToAddress( 119 | projectDetailCredential, 120 | protocolStakeCredential 121 | ); 122 | 123 | const adaToTreasury = 124 | BigInt(projectUtxo.assets.lovelace) - 125 | INACTIVE_PROJECT_UTXO_ADA - 126 | BigInt(protocolParams.discountCentPrice) * PROJECT_DELIST_DISCOUNT_CENTS; 127 | 128 | tx = tx 129 | .collectFrom( 130 | [projectUtxo], 131 | S.toCbor(S.toData({ case: "FinalizeDelist" }, ProjectRedeemer)) 132 | ) 133 | .payToContract( 134 | outputProjectAddress, 135 | { inline: S.toCbor(S.toData(outputProjectDatum, ProjectDatum)) }, 136 | { lovelace: INACTIVE_PROJECT_UTXO_ADA } 137 | ) 138 | .collectFrom( 139 | [projectDetailUtxo], 140 | S.toCbor(S.toData({ case: "Delist" }, ProjectDetailRedeemer)) 141 | ) 142 | .payToContract( 143 | outputProjectDetailAddress, 144 | { inline: projectDetailUtxo.datum }, 145 | { ...projectDetailUtxo.assets } 146 | ); 147 | if (adaToTreasury > 0n) { 148 | const openTreasuryAddress = lucid.utils.credentialToAddress( 149 | openTreasuryCredential, 150 | protocolStakeCredential 151 | ); 152 | 153 | const openTreasuryDatum: OpenTreasuryDatum = { 154 | governorAda: 155 | (adaToTreasury * BigInt(protocolParams.governorShareRatio)) / 156 | RATIO_MULTIPLIER, 157 | tag: { 158 | kind: "TagProjectDelisted", 159 | projectId: projectDatum.projectId, 160 | }, 161 | }; 162 | tx = tx.payToContract( 163 | openTreasuryAddress, 164 | { 165 | inline: S.toCbor(S.toData(openTreasuryDatum, OpenTreasuryDatum)), 166 | }, 167 | { lovelave: adaToTreasury } 168 | ); 169 | } 170 | } 171 | return tx; 172 | } 173 | -------------------------------------------------------------------------------- /tests/treasury/open-treasury-txs.test.ts: -------------------------------------------------------------------------------- 1 | import { Emulator, Lucid, UTxO, Unit } from "lucid-cardano"; 2 | 3 | import { compileOpenTreasuryVScript } from "@/commands/compile-scripts"; 4 | import { SAMPLE_PROTOCOL_NON_SCRIPT_PARAMS } from "@/commands/generate-protocol-params"; 5 | import { PROTOCOL_NFT_TOKEN_NAMES } from "@/contracts/common/constants"; 6 | import { exportScript } from "@/contracts/compile"; 7 | import { addressFromScriptHashes, signAndSubmit } from "@/helpers/lucid"; 8 | import { constructAddress, constructTxOutputId } from "@/helpers/schema"; 9 | import * as S from "@/schema"; 10 | import { ProtocolParamsDatum } from "@/schema/teiki/protocol"; 11 | import { OpenTreasuryDatum } from "@/schema/teiki/treasury"; 12 | import { MIN_UTXO_LOVELACE } from "@/transactions/constants"; 13 | import { 14 | Params, 15 | withdrawAdaTx, 16 | } from "@/transactions/treasury/open-treasury/withdraw-ada"; 17 | 18 | import { 19 | attachUtxos, 20 | generateAccount, 21 | generateBlake2b224Hash, 22 | generateOutRef, 23 | generateScriptAddress, 24 | generateWalletAddress, 25 | } from "../emulator"; 26 | import { generateProtocolRegistry, getRandomLovelaceAmount } from "../utils"; 27 | 28 | const GOVERNOR_ACCOUNT = await generateAccount(); 29 | const ANYONE_ACCOUNT = await generateAccount(); 30 | const emulator = new Emulator([GOVERNOR_ACCOUNT, ANYONE_ACCOUNT]); 31 | const lucid = await Lucid.new(emulator); 32 | 33 | // context 34 | const protocolNftMph = generateBlake2b224Hash(); 35 | const protocolSvHash = generateBlake2b224Hash(); 36 | const refScriptAddress = generateScriptAddress(lucid); 37 | 38 | const stakingManagerAddress = generateWalletAddress(lucid); 39 | const protocolParamsAddress = generateScriptAddress(lucid); 40 | 41 | const openTreasuryVScript = exportScript( 42 | compileOpenTreasuryVScript({ protocolNftMph }) 43 | ); 44 | 45 | const openTreasuryVScriptHash = 46 | lucid.utils.validatorToScriptHash(openTreasuryVScript); 47 | 48 | const openTreasuryVScriptAddress = addressFromScriptHashes( 49 | lucid, 50 | openTreasuryVScriptHash 51 | ); 52 | 53 | const openTreasuryVRefScriptUtxo: UTxO = { 54 | ...generateOutRef(), 55 | address: refScriptAddress, 56 | assets: { lovelace: MIN_UTXO_LOVELACE }, 57 | scriptRef: openTreasuryVScript, 58 | }; 59 | 60 | const registry = generateProtocolRegistry(protocolSvHash, { 61 | openTreasury: openTreasuryVScriptHash, 62 | }); 63 | 64 | const protocolParamsDatum: ProtocolParamsDatum = { 65 | registry, 66 | governorAddress: constructAddress(GOVERNOR_ACCOUNT.address), 67 | stakingManager: constructAddress(stakingManagerAddress).paymentCredential, 68 | ...SAMPLE_PROTOCOL_NON_SCRIPT_PARAMS, 69 | }; 70 | 71 | const protocolParamsNftUnit: Unit = 72 | protocolNftMph + PROTOCOL_NFT_TOKEN_NAMES.PARAMS; 73 | 74 | const protocolParamsUtxo: UTxO = { 75 | ...generateOutRef(), 76 | address: protocolParamsAddress, 77 | assets: { lovelace: MIN_UTXO_LOVELACE, [protocolParamsNftUnit]: 1n }, 78 | datum: S.toCbor(S.toData(protocolParamsDatum, ProtocolParamsDatum)), 79 | }; 80 | 81 | describe("open treasury transactions", () => { 82 | it("withdraw ADA tx - protocol-governor", async () => { 83 | expect.assertions(1); 84 | 85 | lucid.selectWalletFromSeed(GOVERNOR_ACCOUNT.seedPhrase); 86 | 87 | const openTreasuryUtxos = generateOpenTreasuryUtxoList(10); 88 | attachUtxos(emulator, [ 89 | protocolParamsUtxo, 90 | openTreasuryVRefScriptUtxo, 91 | ...openTreasuryUtxos, 92 | ]); 93 | 94 | emulator.awaitBlock(10); 95 | 96 | const params: Params = { 97 | protocolParamsUtxo, 98 | openTreasuryUtxos, 99 | openTreasuryVRefScriptUtxo, 100 | actor: "protocol-governor", 101 | }; 102 | 103 | const tx = withdrawAdaTx(lucid, params); 104 | 105 | const txComplete = await tx.complete(); 106 | 107 | await expect(lucid.awaitTx(await signAndSubmit(txComplete))).resolves.toBe( 108 | true 109 | ); 110 | }); 111 | 112 | it("withdraw ADA tx - any one", async () => { 113 | expect.assertions(1); 114 | 115 | lucid.selectWalletFromSeed(GOVERNOR_ACCOUNT.seedPhrase); 116 | 117 | const openTreasuryUtxos = generateOpenTreasuryUtxoList(10); 118 | attachUtxos(emulator, [ 119 | protocolParamsUtxo, 120 | openTreasuryVRefScriptUtxo, 121 | ...openTreasuryUtxos, 122 | ]); 123 | 124 | emulator.awaitBlock(10); 125 | 126 | const params: Params = { 127 | protocolParamsUtxo, 128 | openTreasuryUtxos, 129 | openTreasuryVRefScriptUtxo, 130 | actor: "anyone", 131 | }; 132 | 133 | const tx = withdrawAdaTx(lucid, params); 134 | 135 | const txComplete = await tx.complete(); 136 | 137 | await expect(lucid.awaitTx(await signAndSubmit(txComplete))).resolves.toBe( 138 | true 139 | ); 140 | }); 141 | }); 142 | 143 | function generateOpenTreasuryUtxo() { 144 | const datum: OpenTreasuryDatum = { 145 | governorAda: getRandomLovelaceAmount(), 146 | tag: { 147 | kind: "TagContinuation", 148 | former: constructTxOutputId(generateOutRef()), 149 | }, 150 | }; 151 | 152 | return { 153 | ...generateOutRef(), 154 | address: openTreasuryVScriptAddress, 155 | assets: { 156 | lovelace: datum.governorAda + getRandomLovelaceAmount(), 157 | }, 158 | datum: S.toCbor(S.toData(datum, OpenTreasuryDatum)), 159 | }; 160 | } 161 | 162 | function generateOpenTreasuryUtxoList(size: number): UTxO[] { 163 | return [...Array(size)].map((_) => generateOpenTreasuryUtxo()); 164 | } 165 | -------------------------------------------------------------------------------- /src/commands/compile-scripts.ts: -------------------------------------------------------------------------------- 1 | import { UplcProgram } from "@hyperionbt/helios"; 2 | import { OutRef } from "lucid-cardano"; 3 | 4 | import getBackingV, { 5 | Params as BackingVParams, 6 | } from "@/contracts/backing/backing.v/main"; 7 | import getProofOfBackingMp, { 8 | Params as ProofOfBackingMpParams, 9 | } from "@/contracts/backing/proof-of-backing.mp/main"; 10 | import { compile } from "@/contracts/compile"; 11 | import getTeikiPlantNft from "@/contracts/meta-protocol/teiki-plant.nft/main"; 12 | import getTeikiPlantV, { 13 | Params as TeikiPlantVParams, 14 | } from "@/contracts/meta-protocol/teiki-plant.v/main"; 15 | import getTeikiMp from "@/contracts/meta-protocol/teiki.mp/main"; 16 | import getProjectDetailV, { 17 | Params as ProjectDetailVParams, 18 | } from "@/contracts/project/project-detail.v/main"; 19 | import getProjectScriptV, { 20 | Params as ProjectScriptVParams, 21 | } from "@/contracts/project/project-script.v/main"; 22 | import getProjectAt, { 23 | Params as ProjectAtMpParams, 24 | } from "@/contracts/project/project.at/main"; 25 | import getProjectSv, { 26 | Params as ProjectSvParams, 27 | } from "@/contracts/project/project.sv/main"; 28 | import getProjectV, { 29 | Params as ProjectVParams, 30 | } from "@/contracts/project/project.v/main"; 31 | import getProtocolParamsV, { 32 | Params as ProtocolParamsVParams, 33 | } from "@/contracts/protocol/protocol-params.v/main"; 34 | import getProtocolProposalV, { 35 | Params as ProtocolProposalVParams, 36 | } from "@/contracts/protocol/protocol-proposal.v/main"; 37 | import getProtocolScriptV, { 38 | Params as ProtocolScriptVParams, 39 | } from "@/contracts/protocol/protocol-script.v/main"; 40 | import getProtocolNft, { 41 | Params as ProtocolNftMpParams, 42 | } from "@/contracts/protocol/protocol.nft/main"; 43 | import getProtocolSv, { 44 | Params as ProtocolSvParams, 45 | } from "@/contracts/protocol/protocol.sv/main"; 46 | import getSampleMigrateTokenMp, { 47 | Params as SampleMigrateTokenMpParams, 48 | } from "@/contracts/sample-migration/sample-migrate-token.mp/main"; 49 | import getDedicatedTreasuryV, { 50 | Params as DedicatedTreasuryVParams, 51 | } from "@/contracts/treasury/dedicated-treasury.v/main"; 52 | import getOpenTreasuryV, { 53 | Params as OpenTreasuryVParams, 54 | } from "@/contracts/treasury/open-treasury.v/main"; 55 | import getSharedTreasuryV, { 56 | Params as SharedTreasuryVParams, 57 | } from "@/contracts/treasury/shared-treasury.v/main"; 58 | import { Hex } from "@/types"; 59 | 60 | export function compileProtocolNftScript( 61 | params: ProtocolNftMpParams 62 | ): UplcProgram { 63 | return compile(getProtocolNft(params)); 64 | } 65 | 66 | export function compileProtocolParamsVScript( 67 | params: ProtocolParamsVParams 68 | ): UplcProgram { 69 | return compile(getProtocolParamsV(params)); 70 | } 71 | 72 | export function compileProtocolScriptVScript( 73 | params: ProtocolScriptVParams 74 | ): UplcProgram { 75 | return compile(getProtocolScriptV(params)); 76 | } 77 | 78 | export function compileProtocolProposalVScript( 79 | params: ProtocolProposalVParams 80 | ): UplcProgram { 81 | return compile(getProtocolProposalV(params)); 82 | } 83 | 84 | export function compileProtocolSvScript(params: ProtocolSvParams): UplcProgram { 85 | return compile(getProtocolSv(params)); 86 | } 87 | 88 | export function compileProjectsAtMpScript( 89 | params: ProjectAtMpParams 90 | ): UplcProgram { 91 | return compile(getProjectAt(params)); 92 | } 93 | 94 | export function compileProjectVScript(params: ProjectVParams): UplcProgram { 95 | return compile(getProjectV(params)); 96 | } 97 | 98 | export function compileProjectDetailVScript( 99 | params: ProjectDetailVParams 100 | ): UplcProgram { 101 | return compile(getProjectDetailV(params)); 102 | } 103 | 104 | export function compileProjectScriptVScript( 105 | params: ProjectScriptVParams 106 | ): UplcProgram { 107 | return compile(getProjectScriptV(params)); 108 | } 109 | 110 | export function compileProjectSvScript(params: ProjectSvParams): UplcProgram { 111 | return compile(getProjectSv(params)); 112 | } 113 | 114 | export function compileTeikiPlantVScript( 115 | params: TeikiPlantVParams 116 | ): UplcProgram { 117 | return compile(getTeikiPlantV(params)); 118 | } 119 | 120 | export function compileTeikiPlantNftScript( 121 | teikiPlantSeed: OutRef 122 | ): UplcProgram { 123 | return compile(getTeikiPlantNft({ teikiPlantSeed })); 124 | } 125 | 126 | export function compileTeikiMpScript({ 127 | teikiPlantNftMph, 128 | }: { 129 | teikiPlantNftMph: Hex; 130 | }): UplcProgram { 131 | return compile(getTeikiMp({ teikiPlantNftMph })); 132 | } 133 | 134 | export function compileProofOfBackingMpScript( 135 | params: ProofOfBackingMpParams 136 | ): UplcProgram { 137 | return compile(getProofOfBackingMp(params)); 138 | } 139 | 140 | export function compileBackingVScript(params: BackingVParams): UplcProgram { 141 | return compile(getBackingV(params)); 142 | } 143 | 144 | export function compileDedicatedTreasuryVScript( 145 | params: DedicatedTreasuryVParams 146 | ): UplcProgram { 147 | return compile(getDedicatedTreasuryV(params)); 148 | } 149 | 150 | export function compileOpenTreasuryVScript( 151 | params: OpenTreasuryVParams 152 | ): UplcProgram { 153 | return compile(getOpenTreasuryV(params)); 154 | } 155 | 156 | export function compileSharedTreasuryVScript( 157 | params: SharedTreasuryVParams 158 | ): UplcProgram { 159 | return compile(getSharedTreasuryV(params)); 160 | } 161 | 162 | export function compileSampleMigrateTokenMpScript( 163 | params: SampleMigrateTokenMpParams 164 | ): UplcProgram { 165 | return compile(getSampleMigrateTokenMp(params)); 166 | } 167 | -------------------------------------------------------------------------------- /src/contracts/project/project.sv/main.ts: -------------------------------------------------------------------------------- 1 | import { Hex } from "@/types"; 2 | 3 | import { header, helios, module } from "../../program"; 4 | 5 | export type Params = { 6 | projectId: Hex; 7 | stakingSeed: string; 8 | projectAtMph: Hex; 9 | protocolNftMph: Hex; 10 | }; 11 | 12 | // TODO: @sk-saru unused seed data 13 | export default function main({ 14 | projectId, 15 | stakingSeed, 16 | projectAtMph, 17 | protocolNftMph, 18 | }: Params) { 19 | return helios` 20 | ${header("staking", "sv__project")} 21 | 22 | import { 23 | PROJECT_AT_TOKEN_NAME, 24 | PROJECT_DETAIL_AT_TOKEN_NAME, 25 | PROJECT_SCRIPT_AT_TOKEN_NAME 26 | } from ${module("constants")} 27 | 28 | import { 29 | script_hash_to_staking_credential, 30 | is_tx_authorized_by, 31 | find_pparams_datum_from_inputs 32 | } from ${module("helpers")} 33 | 34 | 35 | import { Datum as PParamsDatum } 36 | from ${module("v__protocol_params__types")} 37 | 38 | import { 39 | Datum as ProjectDatum, 40 | Redeemer as ProjectRedeemer 41 | } from ${module("v__project__types")} 42 | 43 | import { 44 | Datum as ProjectDetailDatum, 45 | Redeemer as ProjectDetailRedeemer 46 | } from ${module("v__project_detail__types")} 47 | 48 | import { Redeemer as ProjectScriptRedeemer } 49 | from ${module("v__project_script__types")} 50 | 51 | const PROJECT_ID: ByteArray = #${projectId} 52 | const STAKING_SEED: ByteArray = #${stakingSeed} 53 | 54 | const PROJECT_AT_MPH: MintingPolicyHash = 55 | MintingPolicyHash::new(#${projectAtMph}) 56 | 57 | const PROJECT_AT_ASSET_CLASS: AssetClass = 58 | AssetClass::new(PROJECT_AT_MPH, PROJECT_AT_TOKEN_NAME) 59 | 60 | const PROJECT_DETAIL_AT_ASSET_CLASS: AssetClass = 61 | AssetClass::new(PROJECT_AT_MPH, PROJECT_DETAIL_AT_TOKEN_NAME) 62 | 63 | const PROJECT_SCRIPT_AT_ASSET_CLASS: AssetClass = 64 | AssetClass::new(PROJECT_AT_MPH, PROJECT_SCRIPT_AT_TOKEN_NAME) 65 | 66 | const PROTOCOL_NFT_MPH: MintingPolicyHash = 67 | MintingPolicyHash::new(#${protocolNftMph}) 68 | 69 | func main(_, ctx: ScriptContext) -> Bool{ 70 | tx: Tx = ctx.tx; 71 | 72 | ctx.get_script_purpose().switch { 73 | 74 | rewarding: Rewarding => { 75 | tx.inputs.any( 76 | (input: TxInput) -> { 77 | output: TxOutput = input.output; 78 | 79 | case_active: Bool = 80 | output.value.get_safe(PROJECT_DETAIL_AT_ASSET_CLASS) == 1 81 | && output.datum.switch { 82 | i: Inline => ProjectDetailDatum::from_data(i.data).project_id == PROJECT_ID, 83 | else => false 84 | } 85 | && ProjectDetailRedeemer::from_data( 86 | tx.redeemers.get( 87 | ScriptPurpose::new_spending(input.output_id) 88 | ) 89 | ).switch { 90 | WithdrawFunds => true, 91 | else => false 92 | }; 93 | 94 | case_inactive: Bool = 95 | output.value.get_safe(PROJECT_SCRIPT_AT_ASSET_CLASS) == 1 96 | && script_hash_to_staking_credential(output.ref_script_hash.unwrap()) 97 | == rewarding.credential; 98 | 99 | case_active || case_inactive 100 | } 101 | ) 102 | }, 103 | 104 | certifying: Certifying => { 105 | certifying.dcert.switch { 106 | 107 | // NOTE: This case is unreachable 108 | // We need this check to generate distinguish stake validators by STAKING_SEED 109 | Register => { 110 | STAKING_SEED == tx.inputs.head.serialize() 111 | }, 112 | 113 | deregister: Deregister => { 114 | tx.inputs.any( 115 | (input: TxInput) -> { 116 | output: TxOutput = input.output; 117 | output.value.get_safe(PROJECT_SCRIPT_AT_ASSET_CLASS) == 1 118 | && script_hash_to_staking_credential(output.ref_script_hash.unwrap()) 119 | == deregister.credential 120 | } 121 | ) 122 | }, 123 | 124 | Delegate => { 125 | pparams_datum: PParamsDatum = 126 | find_pparams_datum_from_inputs(tx.ref_inputs, PROTOCOL_NFT_MPH); 127 | 128 | staking_manager_credential: Credential = 129 | pparams_datum.staking_manager; 130 | governor_credential: Credential = 131 | pparams_datum.governor_address.credential; 132 | 133 | check = (input: TxInput) -> Bool { 134 | output: TxOutput = input.output; 135 | if (output.value.get_safe(PROJECT_AT_ASSET_CLASS) == 1) { 136 | project_datum: ProjectDatum = 137 | output.datum.switch { 138 | i: Inline => ProjectDatum::from_data(i.data), 139 | else => error("Invalid project UTxO: missing inline datum") 140 | }; 141 | project_datum.project_id == PROJECT_ID 142 | && if (project_datum.is_staking_delegation_managed_by_protocol) { 143 | is_tx_authorized_by(tx, staking_manager_credential) 144 | || is_tx_authorized_by(tx, governor_credential) 145 | || ProjectRedeemer::from_data( 146 | tx.redeemers.get(ScriptPurpose::new_spending(input.output_id)) 147 | ).switch { 148 | UpdateStakingDelegationManagement => true, 149 | else => false 150 | } 151 | } else { 152 | is_tx_authorized_by(tx, project_datum.owner_address.credential) 153 | } 154 | } else { 155 | false 156 | } 157 | }; 158 | 159 | tx.ref_inputs.any(check) || tx.inputs.any(check) 160 | }, 161 | 162 | else => false 163 | 164 | } 165 | }, 166 | 167 | else => false 168 | 169 | } 170 | } 171 | `; 172 | } 173 | --------------------------------------------------------------------------------