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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Default owners for all files
* @scasplte2
# OttoBot-AI fork β€” no external approval required for iteration
* @ottobot-ai
18 changes: 6 additions & 12 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
## Changes
-
## Description
<!-- Brief description of changes -->

## Type
- [ ] Bug fix
- [ ] New feature
- [ ] Infrastructure/config change
- [ ] Documentation
## Related Issues
<!-- Link to related issues: Closes #123 -->

## Testing
- [ ] Tested locally
## Checklist
- [ ] Tests pass locally
- [ ] CI passes

## Deployment Notes
<!-- Any special steps needed after merge? -->
8 changes: 4 additions & 4 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ jobs:
fetch-tags: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

- name: Log in to Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
Expand Down Expand Up @@ -92,7 +92,7 @@ jobs:

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
Expand All @@ -104,7 +104,7 @@ jobs:
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' || github.event.release.tag_name != '' }}

- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
push: true
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ jobs:

- name: Upload node logs
if: failure() || cancelled()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: e2e-logs-${{ github.run_number }}
path: /tmp/ci-logs/
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.7.11"
".": "0.7.13"
}
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## [0.7.13](https://github.com/scasplte2/ottochain/compare/v0.7.12...v0.7.13) (2026-03-13)


### Bug Fixes

* **docker:** pass --l0-token-identifier to ML0 validators ([#145](https://github.com/scasplte2/ottochain/issues/145)) ([d899c73](https://github.com/scasplte2/ottochain/commit/d899c735284cfe8c14ce913a0e8d3e226ff0c70b))

## [0.7.12](https://github.com/scasplte2/ottochain/compare/v0.7.11...v0.7.12) (2026-03-11)


### Bug Fixes

* don't pass --l0-token-identifier to ML0 ([#136](https://github.com/scasplte2/ottochain/issues/136)) ([7217607](https://github.com/scasplte2/ottochain/commit/721760702016d445a567d4052f23ffb4b0c301b7))

## [0.7.11](https://github.com/scasplte2/ottochain/compare/v0.7.10...v0.7.11) (2026-03-01)


Expand Down
23 changes: 1 addition & 22 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -79,29 +79,8 @@ lazy val buildInfoSettings = Seq(
lazy val root = (project in file("."))
.settings(
name := "ottochain"
).aggregate(proto, models, sharedData, currencyL0, currencyL1, dataL1)
).aggregate(models, sharedData, currencyL0, currencyL1, dataL1)

lazy val proto = (project in file("modules/proto"))
.dependsOn(models)
.settings(
commonSettings,
commonTestSettings,
name := "ottochain-proto",
Compile / PB.targets := Seq(
scalapb.gen(flatPackage = true) -> (Compile / sourceManaged).value / "scalapb",
scalapb.validate.gen() -> (Compile / sourceManaged).value / "scalapb"
),
Compile / PB.protoSources := Seq(
(Compile / sourceDirectory).value / "protobuf"
),
libraryDependencies ++= Seq(
Libraries.scalapbRuntime,
Libraries.scalapbRuntime % "protobuf",
Libraries.scalapbValidateCore,
Libraries.scalapbValidateCore % "protobuf",
Libraries.scalapbCirce
)
)

lazy val models = (project in file("modules/models"))
.settings(
Expand Down
8 changes: 7 additions & 1 deletion docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,14 @@ case "${LAYER,,}" in
fi

# Token ID for metagraph layers
# ML0 run-genesis does NOT accept --l0-token-identifier, but run-validator REQUIRES it.
# CL1/DL1 always need it.
if [ -n "${CL_TOKEN_ID}" ]; then
ARGS="${ARGS} --l0-token-identifier ${CL_TOKEN_ID}"
if [ "${LAYER,,}" = "ml0" ] && [ "${RUN_MODE}" = "run-genesis" ]; then
echo "Skipping --l0-token-identifier for ML0 genesis mode"
else
ARGS="${ARGS} --l0-token-identifier ${CL_TOKEN_ID}"
fi
fi
;;
esac
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package xyz.kd5ujc.schema.token

/**
* 4-bit TDEG token behavior matrix.
*
* Encoding:
* Bit 3 (8): T = Transferable β€” can change owner
* Bit 2 (4): D = Divisible β€” can be split/merged
* Bit 1 (2): E = Expirable β€” has time-based validity
* Bit 0 (1): G = Governable β€” subject to validator policy
*
* TokenBehavior.value = TΓ—8 + DΓ—4 + EΓ—2 + GΓ—1 (0..15)
*
* Cross-language equivalence: matches TypeScript SDK PR #45.
* ⚠️ Guard difference: Scala uses `sequenceNumber`; TypeScript uses `$ordinal`
* (TypeScript has a latent bug β€” $ordinal defaults to 0 in JLVM context).
*/
sealed trait TokenBehavior {
def value: Int
def name: String

final def isTransferable: Boolean = (value & TokenBehaviorFlags.Transferable) != 0
final def isDivisible: Boolean = (value & TokenBehaviorFlags.Divisible) != 0
final def isExpirable: Boolean = (value & TokenBehaviorFlags.Expirable) != 0
final def isGovernable: Boolean = (value & TokenBehaviorFlags.Governable) != 0
}

object TokenBehavior {

// ── Type 0–7: Soulbound (T=0) ──────────────────────────────────────────────

/** Type 0: T=0, D=0, E=0, G=0 β€” Permanent soulbound badge */
case object SoulboundReceipt extends TokenBehavior {
val value = 0; val name = "SOULBOUND_RECEIPT"
}

/** Type 1: T=0, D=0, E=0, G=1 β€” Governed membership badge */
case object GovernedBadge extends TokenBehavior {
val value = 1; val name = "GOVERNED_BADGE"
}

/** Type 2: T=0, D=0, E=1, G=0 β€” Time-limited credential */
case object ExpirableCredential extends TokenBehavior {
val value = 2; val name = "EXPIRABLE_CREDENTIAL"
}

/** Type 3: T=0, D=0, E=1, G=1 β€” Professional license */
case object GovernedLicense extends TokenBehavior {
val value = 3; val name = "GOVERNED_LICENSE"
}

/** Type 4: T=0, D=1, E=0, G=0 β€” Accumulated reputation score */
case object LoyaltyPoints extends TokenBehavior {
val value = 4; val name = "LOYALTY_POINTS"
}

/** Type 5: T=0, D=1, E=0, G=1 β€” Governed allocation score */
case object GovernedAllocation extends TokenBehavior {
val value = 5; val name = "GOVERNED_ALLOCATION"
}

/** Type 6: T=0, D=1, E=1, G=0 β€” Expirable loyalty points */
case object ExpirablePoints extends TokenBehavior {
val value = 6; val name = "EXPIRABLE_POINTS"
}

/** Type 7: T=0, D=1, E=1, G=1 β€” Governed expirable allocation */
case object GovernedExpirablePoints extends TokenBehavior {
val value = 7; val name = "GOVERNED_EXPIRABLE_POINTS"
}

// ── Type 8–15: Transferable (T=1) ──────────────────────────────────────────

/** Type 8: T=1, D=0, E=0, G=0 β€” Pure NFT */
case object NFT extends TokenBehavior {
val value = 8; val name = "NFT"
}

/** Type 9: T=1, D=0, E=0, G=1 β€” Governed collectible */
case object GovernedNFT extends TokenBehavior {
val value = 9; val name = "GOVERNED_NFT"
}

/** Type 10: T=1, D=0, E=1, G=0 β€” Event ticket */
case object ExpirableNFT extends TokenBehavior {
val value = 10; val name = "EXPIRABLE_NFT"
}

/** Type 11: T=1, D=0, E=1, G=1 β€” Governed ticket */
case object GovernedExpirableNFT extends TokenBehavior {
val value = 11; val name = "GOVERNED_EXPIRABLE_NFT"
}

/** Type 12: T=1, D=1, E=0, G=0 β€” Fungible utility token (ERC-20) */
case object FungibleToken extends TokenBehavior {
val value = 12; val name = "FUNGIBLE_TOKEN"
}

/** Type 13: T=1, D=1, E=0, G=1 β€” Stablecoin / regulated token */
case object GovernedFungibleToken extends TokenBehavior {
val value = 13; val name = "GOVERNED_FUNGIBLE_TOKEN"
}

/** Type 14: T=1, D=1, E=1, G=0 β€” Airline miles / subscription credits */
case object ExpirableFungibleToken extends TokenBehavior {
val value = 14; val name = "EXPIRABLE_FUNGIBLE_TOKEN"
}

/** Type 15: T=1, D=1, E=1, G=1 β€” Full-featured financial instrument */
case object GovernedExpirableFungible extends TokenBehavior {
val value = 15; val name = "GOVERNED_EXPIRABLE_FUNGIBLE"
}

// ── Lookup ─────────────────────────────────────────────────────────────────

val all: List[TokenBehavior] = List(
SoulboundReceipt,
GovernedBadge,
ExpirableCredential,
GovernedLicense,
LoyaltyPoints,
GovernedAllocation,
ExpirablePoints,
GovernedExpirablePoints,
NFT,
GovernedNFT,
ExpirableNFT,
GovernedExpirableNFT,
FungibleToken,
GovernedFungibleToken,
ExpirableFungibleToken,
GovernedExpirableFungible
)

def fromInt(value: Int): Option[TokenBehavior] =
all.find(_.value == value)

def unsafeFromInt(value: Int): TokenBehavior =
fromInt(value).getOrElse(
throw new IllegalArgumentException(s"Invalid TokenBehavior value: $value (must be 0–15)")
)

/** Construct behavior from individual TDEG flags */
def fromFlags(transferable: Boolean, divisible: Boolean, expirable: Boolean, governable: Boolean): TokenBehavior = {
val v =
(if (transferable) 8 else 0) |
(if (divisible) 4 else 0) |
(if (expirable) 2 else 0) |
(if (governable) 1 else 0)
unsafeFromInt(v)
}

// ── Operation Legality ─────────────────────────────────────────────────────

def isOperationAllowed(behavior: TokenBehavior, op: TokenOperation): Boolean = op match {
case TokenOperation.Mint => true // Always allowed (guard may restrict)
case TokenOperation.Burn => true // Always allowed (even when expired)
case TokenOperation.Transfer => behavior.isTransferable
case TokenOperation.Split => behavior.isDivisible
case TokenOperation.Merge => behavior.isDivisible
case TokenOperation.SetPolicy => behavior.isGovernable
case TokenOperation.Expire => behavior.isExpirable
case TokenOperation.Extend => behavior.isExpirable
}
}
Loading
Loading