How to setup a xlm.sh site
xlm.sh is a gateway for .xlm websites, providing a seamless experience for accessing decentralized content. It simplifies the process of hosting and visiting dApps and static sites built on IPFS/IPNS, making it easier for users to interact with the decentralized ecosystem.
To set up a xlm.sh site, only a few steps are required:
We will guide you through the process of setting up a xlm.sh site in the following sections.
Get a Stellar Soroban Domain
To start setting up your xlm.sh site, you first need to obtain a Soroban Domain. Here's how:
- Visit the official Soroban Domains website at https://app.sorobandomains.org/
- Search and select your desired domain name
- Set up the domain address:
- Point your domain to an address you control
- The simplest way is to click "Use owner address" to automatically use your current wallet address
- Complete the registration process
This domain will serve as your personal identifier in the Soroban ecosystem, replacing the traditional long string of characters with a more user-friendly name. Here we have got a Stellar Soroban Domain: example.xlm
.
Upload your content to IPFS
To upload content to IPFS, you can use either IPFS Desktop or the command line tools. However, for beginners, using an IPFS Pinning Service might be a simpler option. There are several providers offering these services, including: web3.storage, Pinata and Filebase, among others.
Let's take web3.storage as an example:
- Create an account to get free quota
- Use their simple web interface to upload your website files directly - no command line required
- Once uploaded, you'll receive a CID (Content Identifier)
- This CID is your website's unique address on IPFS
For example, after uploading our site content we received the CID: bafybeifq2rzpqnqrsdupncmkmhs3ckxxjhuvdcbvydkgvch3ms24k5lo7q
.
Set up IPFS content for your Stellar account
Now that we have both a Stellar Soroban Domain and an IPFS CID, we need to link them together so that your website can be accessed through your_domain.xlm.sh
.
Below is a form where you only need to:
- Enter your domain name
- Enter your IPFS CID (or IPNS name)
- Click the confirm button
We will then build a transaction for you and guide you to Stellar Laboratory to complete the signing and submission process.
Set up IPFS content for contract address
If you have pointed your Soroban Domain to a contract address (an address starting with C), then you need to use code to set the IPFS record. Here is an example code snippet:
from enum import Enum
from typing import Optional, Union
from Crypto.Hash import keccak
from stellar_sdk import scval, xdr, MuxedAccount, Keypair, Network
from stellar_sdk.contract import AssembledTransaction, ContractClient
IPFS_KEY = "ipfs"
IPNS_KEY = "ipns"
class ValueKind(Enum):
String = "String"
Bytes = "Bytes"
Number = "Number"
class Value:
def __init__(
self,
kind: ValueKind,
string: Optional[bytes] = None,
bytes: Optional[bytes] = None,
number: Optional[int] = None,
):
self.kind = kind
self.string = string
self.bytes = bytes
self.number = number
def to_scval(self) -> xdr.SCVal:
if self.kind == ValueKind.String:
assert self.string is not None
return scval.to_enum(self.kind.name, scval.to_string(self.string))
if self.kind == ValueKind.Bytes:
assert self.bytes is not None
return scval.to_enum(self.kind.name, scval.to_bytes(self.bytes))
if self.kind == ValueKind.Number:
assert self.number is not None
return scval.to_enum(self.kind.name, scval.to_int128(self.number))
raise ValueError(f"Invalid kind: {self.kind}")
@classmethod
def from_scval(cls, val: xdr.SCVal):
elements = scval.from_enum(val)
kind = ValueKind(elements[0])
if kind == ValueKind.String:
assert elements[1] is not None and isinstance(elements[1], xdr.SCVal)
return cls(kind, string=scval.from_string(elements[1]))
if kind == ValueKind.Bytes:
assert elements[1] is not None and isinstance(elements[1], xdr.SCVal)
return cls(kind, bytes=scval.from_bytes(elements[1]))
if kind == ValueKind.Number:
assert elements[1] is not None and isinstance(elements[1], xdr.SCVal)
return cls(kind, number=scval.from_int128(elements[1]))
raise ValueError(f"Invalid kind: {kind}")
def __eq__(self, other: object) -> bool:
if not isinstance(other, Value):
return NotImplemented
if self.kind != other.kind:
return False
if self.kind == ValueKind.String:
return self.string == other.string
if self.kind == ValueKind.Bytes:
return self.bytes == other.bytes
if self.kind == ValueKind.Number:
return self.number == other.number
return True
def __hash__(self) -> int:
if self.kind == ValueKind.String:
return hash((self.kind, self.string))
if self.kind == ValueKind.Bytes:
return hash((self.kind, self.bytes))
if self.kind == ValueKind.Number:
return hash((self.kind, self.number))
return hash(self.kind)
class Client(ContractClient):
def set(
self,
node: bytes,
key: str,
value: Value,
source: Union[str, MuxedAccount],
signer: Optional[Keypair] = None,
base_fee: int = 100,
transaction_timeout: int = 300,
submit_timeout: int = 30,
simulate: bool = True,
restore: bool = True,
) -> AssembledTransaction[None]:
return self.invoke(
"set",
[scval.to_bytes(node), scval.to_symbol(key), value.to_scval()],
parse_result_xdr_fn=lambda _: None,
source=source,
signer=signer,
base_fee=base_fee,
transaction_timeout=transaction_timeout,
submit_timeout=submit_timeout,
simulate=simulate,
restore=restore,
)
def get(
self,
node: bytes,
key: str,
source: Union[str, MuxedAccount],
signer: Optional[Keypair] = None,
base_fee: int = 100,
transaction_timeout: int = 300,
submit_timeout: int = 30,
simulate: bool = True,
restore: bool = True,
) -> AssembledTransaction[Optional[Value]]:
return self.invoke(
"get",
[scval.to_bytes(node), scval.to_symbol(key)],
parse_result_xdr_fn=lambda v: (
Value.from_scval(v)
if v.type != xdr.SCValType.SCV_VOID
else scval.from_void(v)
),
source=source,
signer=signer,
base_fee=base_fee,
transaction_timeout=transaction_timeout,
submit_timeout=submit_timeout,
simulate=simulate,
restore=restore,
)
def parse_domain(domain: str, sub_domain: str = None) -> bytes:
"""
This function takes a domain and generates the "node" value of the parsed domain.
This "node" value can be used to fetch data from the contract (either if is a Record or a SubRecord)
"""
record = keccak.new(digest_bits=256)
record.update(keccak.new(digest_bits=256).update("xlm".encode()).digest())
record.update(keccak.new(digest_bits=256).update(domain.encode()).digest())
if sub_domain:
sub_record = keccak.new(digest_bits=256)
sub_record.update(keccak.new(digest_bits=256).update(record.digest()).digest())
sub_record.update(
keccak.new(digest_bits=256).update(sub_domain.encode()).digest()
)
return sub_record.digest()
else:
return record.digest()
if __name__ == "__main__":
client = Client(
"CDH2T2CBGFPFNVRWFK4XJIRP6VOWSVTSDCRBCJ2TEIO22GADQP6RG3Y6",
"https://mainnet.sorobanrpc.com",
Network.PUBLIC_NETWORK_PASSPHRASE,
)
# You need to set the below values
kp = Keypair.from_secret("S...")
domain = "scfdemo" # domain name
record_type = IPFS_KEY # IPFS_KEY or IPNS_KEY
record = "bafybeicqkdg3ljrpeja5gfnz6zq65egxwgpsyv6wfrrltiizo6d7srkhge" # IPFS CID
# https://github.com/Creit-Tech/sorobandomains-sc/blob/main/contracts/key-value-db/src/utils.rs#L70
# subdomain not supported for contract address now.
subdomain = None
node = parse_domain(domain, subdomain)
value = Value(ValueKind.String, string=record.encode("utf-8"))
try:
client.set(node, record_type, value, kp.public_key, kp).sign_and_submit()
print("Success")
except Exception as e:
print(f"Error: {e}")
resp = client.get(node, record_type, kp.public_key).result()
print(resp.string)